diff --git a/src/App.tsx b/src/App.tsx
index dd963c7..07a49c4 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -1,13 +1,23 @@
-import { appAtom, appStore, menuDataAtom } from '@/store/system.ts'
+import { AppContextProvider } from '@/context.ts'
+import { initI18n } from '@/i18n.ts'
+import { appAtom, appStore, menuDataAtom, changeLanguage } from '@/store/system.ts'
+import { IAppData } from '@/types'
+import { ConfigProvider } from 'antd'
import { Provider, useAtom, useAtomValue } from 'jotai'
import './App.css'
+import { useEffect } from 'react'
import { RootProvider } from './routes.tsx'
+
function App() {
- const [ , ] = useAtom(appAtom)
+ const [ appData, ] = useAtom(appAtom)
const { data, isError, isPending } = useAtomValue(menuDataAtom)
+ useEffect(() => {
+ initI18n()
+ }, [])
+
if (isError) {
return
Error
}
@@ -17,9 +27,18 @@ function App() {
}
return (
-
-
-
+
+
+
+
+
+
+
)
}
diff --git a/src/components/error-boundary/index.tsx b/src/components/error-boundary/index.tsx
index 74b4509..be03391 100644
--- a/src/components/error-boundary/index.tsx
+++ b/src/components/error-boundary/index.tsx
@@ -17,6 +17,7 @@ export class ErrorBoundary extends React.Component<
}
render() {
+
if (this.state.hasError) {
// You can render any custom fallback UI
return (
diff --git a/src/components/error/403.tsx b/src/components/error/403.tsx
index 34bfbfd..0451db5 100644
--- a/src/components/error/403.tsx
+++ b/src/components/error/403.tsx
@@ -1,21 +1,22 @@
+import { useTranslation } from '@/i18n.ts'
import { useNavigate } from '@tanstack/react-router'
import { Button, Result } from 'antd'
const NotPermission = () => {
const navigate = useNavigate()
-
+ const { t } = useTranslation()
return (
navigate({
to: '../'
})}>
- Go Back
+ {t('route.goBack')}
}
/>
diff --git a/src/components/error/404.tsx b/src/components/error/404.tsx
index 9407ab3..fad1e9b 100644
--- a/src/components/error/404.tsx
+++ b/src/components/error/404.tsx
@@ -1,21 +1,23 @@
+import { useTranslation } from '@/i18n.ts'
import { useNavigate } from '@tanstack/react-router'
import { Button, Result } from 'antd'
const NotFound = () => {
const navigate = useNavigate()
+ const { t } = useTranslation()
return (
navigate({
to: '../'
})}>
- Go Back
+ {t('route.goBack')}
}
/>
diff --git a/src/components/error/error.tsx b/src/components/error/error.tsx
index 83c83b2..a780672 100644
--- a/src/components/error/error.tsx
+++ b/src/components/error/error.tsx
@@ -1,7 +1,8 @@
import { Result } from 'antd'
-
+import { useTranslation } from '@/i18n.ts'
const ErrorPage = ({ error }: { error: any, reset?: string }) => {
+ const { t } = useTranslation()
return (
{
background: '#fff',
}}
status={'error'}
- title="错误信息"
+ title={t('error.error.title')}
extra={
<>
LocalData[];
+ onItemClick?: (params: ClickParam) => void;
+ className?: string;
+ reload?: boolean;
+ icon?: React.ReactNode;
+ style?: React.CSSProperties;
+}
+
+const defaultLangUConfigMap = {
+ 'en-US': {
+ lang: 'en-US',
+ label: 'English',
+ icon: '🇺🇸',
+ title: 'Language'
+ },
+ 'zh-CN': {
+ lang: 'zh-CN',
+ label: '简体中文',
+ icon: '🇨🇳',
+ title: '语言'
+ },
+}
+
+export interface HeaderDropdownProps extends DropDownProps {
+ overlayClassName?: string;
+ placement?:
+ | 'bottomLeft'
+ | 'bottomRight'
+ | 'topLeft'
+ | 'topCenter'
+ | 'topRight'
+ | 'bottomCenter';
+}
+
+const HeaderDropdown: React.FC
= ({
+ overlayClassName: cls,
+ ...restProps
+ }) => (
+
+)
+
+export const SelectLang = memo((props: SelectLangProps) => {
+
+ const ctx = useAppContext()
+
+ const {
+ globalIconClassName,
+ postLocalesData,
+ onItemClick,
+ icon,
+ style,
+ reload,
+ ...restProps
+ } = props
+ const [ selectedLang, setSelectedLang ] = useState(() => ctx.appData.language)
+
+ const changeLang = ({ key }: ClickParam): void => {
+ ctx.changeLanguage(key, reload)
+ setSelectedLang(key)
+ }
+
+ const defaultLangUConfig = Object.values(defaultLangUConfigMap)
+
+ const allLangUIConfig =
+ postLocalesData?.(defaultLangUConfig) || defaultLangUConfig
+ const handleClick = onItemClick
+ ? (params: ClickParam) => onItemClick(params)
+ : changeLang
+
+ const menuItemStyle = { minWidth: '160px' }
+ const menuItemIconStyle = { marginRight: '8px' }
+
+ const langMenu = {
+ selectedKeys: [ selectedLang ],
+ onClick: handleClick,
+ items: allLangUIConfig.map((localeObj) => ({
+ key: localeObj.lang || localeObj.key,
+ style: menuItemStyle,
+ label: (
+ <>
+
+ {localeObj?.icon || '🌐'}
+
+ {localeObj?.label || 'zh-CN'}
+ >
+ ),
+ })),
+ }
+
+ const dropdownProps = { menu: langMenu }
+
+ const inlineStyle = {
+ cursor: 'pointer',
+ padding: '12px',
+ display: 'inline-flex',
+ alignItems: 'center',
+ justifyContent: 'center',
+ fontSize: 18,
+ verticalAlign: 'middle',
+ ...style,
+ }
+
+ return (
+
+
+
+ {icon ?
+ icon : (
+
+ )}
+
+
+
+ )
+})
+
+export default SelectLang
\ No newline at end of file
diff --git a/src/context.ts b/src/context.ts
new file mode 100644
index 0000000..7dcb15f
--- /dev/null
+++ b/src/context.ts
@@ -0,0 +1,26 @@
+import { IAppData } from '@/types'
+import React, { createContext, ProviderProps, useContext } from 'react'
+import { t } from 'i18next'
+
+export interface IAppContextValue {
+ get appData(): IAppData
+
+ changeLanguage: (lang: string, reload?: boolean) => void
+
+ t: typeof t
+}
+
+export const AppContext = createContext({} as unknown as any)
+
+export const AppContextProvider = ({ value, children }: ProviderProps>) => {
+ return React.createElement(AppContext.Provider, {
+ value: {
+ ...value,
+ t,
+ } as any
+ }, children)
+}
+
+export const useAppContext = () => {
+ return useContext(AppContext)
+}
\ No newline at end of file
diff --git a/src/hooks/useTranslation.ts b/src/hooks/useTranslation.ts
new file mode 100644
index 0000000..bc1eb5e
--- /dev/null
+++ b/src/hooks/useTranslation.ts
@@ -0,0 +1 @@
+export * from 'react-i18next'
\ No newline at end of file
diff --git a/src/i18n.ts b/src/i18n.ts
new file mode 100644
index 0000000..d7ca59d
--- /dev/null
+++ b/src/i18n.ts
@@ -0,0 +1,52 @@
+import { changeLanguage } from '@/store/system.ts'
+import i18n, { InitOptions } from 'i18next'
+import LanguageDetector from 'i18next-browser-languagedetector'
+import { initReactI18next, useTranslation } from 'react-i18next'
+import { zh, en } from './locales'
+
+const detectionOptions = {
+ // 探测器的选项
+ order: [ 'querystring', 'cookie', 'localStorage', 'navigator', 'htmlTag' ],
+ lookupQuerystring: 'lng',
+ lookupCookie: 'i18next',
+ lookupLocalStorage: 'i18nextLng',
+ caches: [ 'localStorage', 'cookie' ],
+ excludeCacheFor: [ 'cimode' ], // 语言探测模式中排除缓存的语言
+}
+
+
+export const initI18n = (options?: InitOptions) => {
+
+ i18n.on('initialized', () => {
+ const currentLanguage = i18n.language
+ changeLanguage(currentLanguage)
+ })
+
+ return i18n
+ .use(initReactI18next)
+ .use(LanguageDetector)
+ .init({
+ resources: {
+ en: {
+ translation: en.default,
+ },
+ zh: {
+ translation: zh.default,
+ },
+ },
+ fallbackLng: 'zh',
+ debug: true,
+ detection: detectionOptions,
+ interpolation: {
+ escapeValue: false,
+ },
+ ...options,
+ })
+
+
+}
+
+export {
+ useTranslation
+}
+export default i18n
\ No newline at end of file
diff --git a/src/layout/RootLayout.tsx b/src/layout/RootLayout.tsx
index a2f80c6..01bef06 100644
--- a/src/layout/RootLayout.tsx
+++ b/src/layout/RootLayout.tsx
@@ -1,6 +1,7 @@
import PageBreadcrumb from '@/components/breadcrumb'
-import { ErrorBoundary } from '@/components/error-boundary'
import ErrorPage from '@/components/error/error.tsx'
+import SelectLang from '@/components/select-lang'
+import { useTranslation } from '@/i18n.ts'
import { MenuItem } from '@/types'
import { ProConfigProvider, ProLayout, } from '@ant-design/pro-components'
import { CatchBoundary, Link, Outlet, useRouteContext } from '@tanstack/react-router'
@@ -34,6 +35,7 @@ const getBreadcrumbData = (menuData: MenuItem[], pathname: string) => {
export default () => {
+ const { t } = useTranslation()
const { menuData } = useRouteContext({
from: undefined,
strict: false,
@@ -96,7 +98,7 @@ export default () => {
{
key: 'logout',
icon: ,
- label: '退出登录',
+ label: t('app.header.logout'),
},
],
}}
@@ -109,7 +111,9 @@ export default () => {
actionsRender={(props) => {
if (props.isMobile) return []
if (typeof window === 'undefined') return []
- return []
+ return [
+
+ ]
}}
menuRender={(_, defaultDom) => (
<>
@@ -135,7 +139,6 @@ export default () => {
'siderMenuType': 'group',
// layout: 'side',
}}
- ErrorBoundary={ErrorBoundary}
>
diff --git a/src/locales/index.ts b/src/locales/index.ts
new file mode 100644
index 0000000..4770686
--- /dev/null
+++ b/src/locales/index.ts
@@ -0,0 +1,2 @@
+export * as zh from './lang/zh-CN.ts'
+export * as en from './lang/en-US.ts'
\ No newline at end of file
diff --git a/src/locales/lang/en-US.ts b/src/locales/lang/en-US.ts
new file mode 100644
index 0000000..d31a2f1
--- /dev/null
+++ b/src/locales/lang/en-US.ts
@@ -0,0 +1,39 @@
+import antdEN from 'antd/locale/en_US'
+
+export default {
+ ...antdEN,
+
+ error: {
+ '404': {
+ title: 'not fund',
+ message: 'Sorry, not found this page.'
+ },
+ '403': {
+ title: 'not authorized',
+ message: 'Sorry, you are not authorized to access this page.'
+ },
+ 'error': {
+ title: 'error info',
+ },
+ },
+ route: {
+ goBack: 'Go Back',
+ },
+ app: {
+ header: {
+ logout: 'logout',
+ }
+ },
+ home: {
+ welcome: 'Welcome to'
+ },
+ tabs: {
+ refresh: 'Refresh',
+ maximize: 'Maximize',
+ closeCurrent: 'Close current',
+ closeLeft: 'Close Left',
+ closeRight: 'Close Right',
+ closeOther: 'Close other',
+ closeAll: 'Close All'
+ }
+}
diff --git a/src/locales/lang/zh-CN.ts b/src/locales/lang/zh-CN.ts
new file mode 100644
index 0000000..9f22189
--- /dev/null
+++ b/src/locales/lang/zh-CN.ts
@@ -0,0 +1,38 @@
+import antdZh from 'antd/locale/zh_CN'
+
+export default {
+ ...antdZh,
+ error: {
+ '404': {
+ title: '无法找到',
+ message: '找不到此页面'
+ },
+ '403': {
+ title: '没有权限',
+ message: '对不起,您没有权限查看此页面。'
+ },
+ 'error': {
+ title: '错误信息',
+ },
+ },
+ route: {
+ goBack: '返回',
+ },
+ app: {
+ header: {
+ logout: '退出登录',
+ }
+ },
+ home: {
+ welcome: '欢迎使用'
+ },
+ tabs: {
+ refresh: '刷新',
+ maximize: '最大化',
+ closeCurrent: '关闭当前',
+ closeLeft: '关闭左侧',
+ closeRight: '关闭右侧',
+ closeOther: '关闭其它',
+ closeAll: '关闭所有'
+ }
+}
diff --git a/src/pages/login/index.tsx b/src/pages/login/index.tsx
index a2ab53e..1df32ec 100644
--- a/src/pages/login/index.tsx
+++ b/src/pages/login/index.tsx
@@ -1,10 +1,11 @@
+import SelectLang from '@/components/select-lang'
import { createFileRoute } from '@tanstack/react-router'
const Login = () => {
return (
- {}
+
)
}
diff --git a/src/store/system.ts b/src/store/system.ts
index 4ac42da..1eb1ff2 100644
--- a/src/store/system.ts
+++ b/src/store/system.ts
@@ -5,7 +5,7 @@ import { atom, createStore } from 'jotai'
import { atomWithQuery } from 'jotai-tanstack-query'
import { atomWithStorage } from 'jotai/utils'
import systemServ from '../service/system.ts'
-
+import { changeLanguage as setLang } from 'i18next'
/**
* app全局状态
@@ -13,42 +13,61 @@ import systemServ from '../service/system.ts'
export const appStore = createStore()
export const appAtom = atomWithStorage>('app', {
- name: 'Crazy Pro',
- version: '1.0.0',
- language: 'zh-CN',
+ name: 'Crazy Pro',
+ version: '1.0.0',
+ language: 'zh-CN',
})
export const getAppData = () => {
- return appStore.get(appAtom)
+ return appStore.get(appAtom)
+}
+
+export const changeLanguage = (lang: string, reload?: boolean) => {
+ if (appStore.get(appAtom).language !== lang) {
+ setLang(lang)
+ updateAppData({ language: lang })
+ if (reload) {
+ window.location.reload()
+ }
+ }
+}
+
+export const updateAppData = (app: Partial) => {
+ appStore.set(appAtom, (prev) => {
+ return {
+ ...prev,
+ ...app,
+ }
+ })
}
export const getToken = () => {
- return appStore.get(appAtom).token
+ return appStore.get(appAtom).token
}
export const setToken = (token: string) => {
- appStore.set(appAtom, { token })
+ updateAppData({ token })
}
export const menuDataAtom = atomWithQuery(() => ({
- queryKey: [ 'menus' ],
- queryFn: async () => {
- if (!isAuthenticated()) {
- return []
- }
- return await systemServ.menus.list()
- },
- select: data => formatMenuData(data as any ?? []),
+ queryKey: [ 'menus' ],
+ queryFn: async () => {
+ if (!isAuthenticated()) {
+ return []
+ }
+ return await systemServ.menus.list()
+ },
+ select: data => formatMenuData(data as any ?? []),
}))
export const selectedMenuIdAtom = atom(0)
export const selectedMenuAtom = atom