Browse Source

增加多语言

main
李金 5 months ago
parent
commit
79a237a6be
  1. 23
      src/App.tsx
  2. 1
      src/components/error-boundary/index.tsx
  3. 9
      src/components/error/403.tsx
  4. 8
      src/components/error/404.tsx
  5. 5
      src/components/error/error.tsx
  6. 152
      src/components/select-lang/index.tsx
  7. 26
      src/context.ts
  8. 1
      src/hooks/useTranslation.ts
  9. 52
      src/i18n.ts
  10. 11
      src/layout/RootLayout.tsx
  11. 2
      src/locales/index.ts
  12. 39
      src/locales/lang/en-US.ts
  13. 38
      src/locales/lang/zh-CN.ts
  14. 3
      src/pages/login/index.tsx
  15. 23
      src/store/system.ts

23
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 <div>Error</div>
}
@ -17,9 +27,18 @@ function App() {
}
return (
<ConfigProvider>
<AppContextProvider value={{
get appData() {
return appData as IAppData
},
changeLanguage
}}>
<Provider store={appStore}>
<RootProvider context={{ menuData: data }}/>
</Provider>
</AppContextProvider>
</ConfigProvider>
)
}

1
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 (

9
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 (
<Result
className="no-permission-page"
status="403"
title="403"
subTitle="Sorry, you are not authorized to access this page."
title={t('error.403.title')}
subTitle={t('error.403.message')}
extra={
<Button type="primary" onClick={() => navigate({
to: '../'
})}>
Go Back
{t('route.goBack')}
</Button>
}
/>

8
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 (
<Result
className="error-page"
status="404"
title="404"
subTitle="Sorry, the page you visited does not exist."
title={t('error.404.title')}
subTitle={t('error.404.message')}
extra={
<Button type="primary" onClick={() => navigate({
to: '../'
})}>
Go Back
{t('route.goBack')}
</Button>
}
/>

5
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 (
<Result
style={{
@ -9,7 +10,7 @@ const ErrorPage = ({ error }: { error: any, reset?: string }) => {
background: '#fff',
}}
status={'error'}
title="错误信息"
title={t('error.error.title')}
extra={
<>
<div

152
src/components/select-lang/index.tsx

@ -0,0 +1,152 @@
import { useAppContext } from '@/context'
import { Dropdown, DropDownProps } from 'antd'
import React, { memo, useState } from 'react'
interface LocalData {
key?: string
lang: string,
label?: string,
icon?: string,
title?: string,
}
interface ClickParam {
key: string
}
interface SelectLangProps {
globalIconClassName?: string;
postLocalesData?: (locales: LocalData[]) => 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<HeaderDropdownProps> = ({
overlayClassName: cls,
...restProps
}) => (
<Dropdown
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: (
<>
<span role="img" aria-label={localeObj?.label || 'zh-CN'} style={menuItemIconStyle}>
{localeObj?.icon || '🌐'}
</span>
{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 (
<HeaderDropdown {...dropdownProps} placement="bottomRight" {...restProps}>
<span className={globalIconClassName} style={inlineStyle}>
<i className="anticon" title={allLangUIConfig[selectedLang]?.title}>
{icon ?
icon : (
<svg
viewBox="0 0 24 24"
focusable="false"
width="1em"
height="1em"
fill="currentColor"
aria-hidden="true"
>
<path d="M0 0h24v24H0z" fill="none"/>
<path
d="M12.87 15.07l-2.54-2.51.03-.03c1.74-1.94 2.98-4.17 3.71-6.53H17V4h-7V2H8v2H1v1.99h11.17C11.5 7.92 10.44 9.75 9 11.35 8.07 10.32 7.3 9.19 6.69 8h-2c.73 1.63 1.73 3.17 2.98 4.56l-5.09 5.02L4 19l5-5 3.11 3.11.76-2.04zM18.5 10h-2L12 22h2l1.12-3h4.75L21 22h2l-4.5-12zm-2.62 7l1.62-4.33L19.12 17h-3.24z "
className="css-c4d79v"
/>
</svg>
)}
</i>
</span>
</HeaderDropdown>
)
})
export default SelectLang

26
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<IAppContextValue>({} as unknown as any)
export const AppContextProvider = ({ value, children }: ProviderProps<Partial<IAppContextValue>>) => {
return React.createElement(AppContext.Provider, {
value: {
...value,
t,
} as any
}, children)
}
export const useAppContext = () => {
return useContext<IAppContextValue>(AppContext)
}

1
src/hooks/useTranslation.ts

@ -0,0 +1 @@
export * from 'react-i18next'

52
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

11
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: <Icon type={'Logout'}/>,
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 [
<SelectLang/>
]
}}
menuRender={(_, defaultDom) => (
<>
@ -135,7 +139,6 @@ export default () => {
'siderMenuType': 'group',
// layout: 'side',
}}
ErrorBoundary={ErrorBoundary}
>
<Outlet/>
</ProLayout>

2
src/locales/index.ts

@ -0,0 +1,2 @@
export * as zh from './lang/zh-CN.ts'
export * as en from './lang/en-US.ts'

39
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'
}
}

38
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: '关闭所有'
}
}

3
src/pages/login/index.tsx

@ -1,10 +1,11 @@
import SelectLang from '@/components/select-lang'
import { createFileRoute } from '@tanstack/react-router'
const Login = () => {
return (
<div>
{}
<SelectLang/>
</div>
)
}

23
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全局状态
@ -23,12 +23,31 @@ export const getAppData = () => {
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<IAppData>) => {
appStore.set(appAtom, (prev) => {
return {
...prev,
...app,
}
})
}
export const getToken = () => {
return appStore.get(appAtom).token
}
export const setToken = (token: string) => {
appStore.set(appAtom, { token })
updateAppData({ token })
}
export const menuDataAtom = atomWithQuery(() => ({

Loading…
Cancel
Save