李金
7 months ago
15 changed files with 396 additions and 39 deletions
-
29src/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
-
59src/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