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(undefined) export const byIdMenuAtom = atomWithQuery((get) => ({ - queryKey: [ 'selectedMenu', get(selectedMenuIdAtom) ], - queryFn: async ({ queryKey: [ , id ] }) => { - return await systemServ.menus.info(id as number) - }, - select: data => data.data, + queryKey: [ 'selectedMenu', get(selectedMenuIdAtom) ], + queryFn: async ({ queryKey: [ , id ] }) => { + return await systemServ.menus.info(id as number) + }, + select: data => data.data, }))