 李金
					
					2 years ago
						李金
					
					2 years ago
					
				
				 15 changed files with 396 additions and 39 deletions
			
			
		- 
					23src/App.tsx
- 
					1src/components/error-boundary/index.tsx
- 
					9src/components/error/403.tsx
- 
					8src/components/error/404.tsx
- 
					5src/components/error/error.tsx
- 
					152src/components/select-lang/index.tsx
- 
					26src/context.ts
- 
					1src/hooks/useTranslation.ts
- 
					52src/i18n.ts
- 
					11src/layout/RootLayout.tsx
- 
					2src/locales/index.ts
- 
					39src/locales/lang/en-US.ts
- 
					38src/locales/lang/zh-CN.ts
- 
					3src/pages/login/index.tsx
- 
					23src/store/system.ts
| @ -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 | ||||
| @ -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) | ||||
|  | } | ||||
| @ -0,0 +1 @@ | |||||
|  | export * from 'react-i18next' | ||||
| @ -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 | ||||
| @ -0,0 +1,2 @@ | |||||
|  | export * as zh from './lang/zh-CN.ts' | ||||
|  | export * as en from './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' | ||||
|  |   } | ||||
|  | } | ||||
| @ -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: '关闭所有' | ||||
|  |   } | ||||
|  | } | ||||
						Write
						Preview
					
					
					Loading…
					
					Cancel
						Save
					
		Reference in new issue