From 7e0773fcb26e0364a3adf7b99d24c0d3203c1454 Mon Sep 17 00:00:00 2001 From: Lei OT Date: Wed, 29 May 2024 17:23:37 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=A4=9A=E8=AF=AD=E8=A8=80,=20?= =?UTF-8?q?=E5=88=87=E6=8D=A2=E8=AF=AD=E8=A8=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 3 + src/hooks/usePresets.js | 42 +++++++++++ src/i18n/index.jsx | 45 ++++++++++++ src/i18n/locales/en.json | 71 +++++++++++++++++++ src/i18n/locales/zh.json | 63 +++++++++++++++++ src/main.jsx | 1 + src/views/App.jsx | 117 ++++++++++++++++--------------- src/views/Language.jsx | 27 +++++++ src/views/Login.jsx | 15 ++-- src/views/Standlone.jsx | 6 +- src/views/reservation/Detail.jsx | 38 +++++----- src/views/reservation/Newest.jsx | 46 ++++++------ 12 files changed, 372 insertions(+), 102 deletions(-) create mode 100644 src/hooks/usePresets.js create mode 100644 src/i18n/index.jsx create mode 100644 src/i18n/locales/en.json create mode 100644 src/i18n/locales/zh.json create mode 100644 src/views/Language.jsx diff --git a/package.json b/package.json index 098a5ef..6753bc9 100644 --- a/package.json +++ b/package.json @@ -11,10 +11,13 @@ "dependencies": { "@react-pdf/renderer": "^3.4.0", "antd": "^5.4.2", + "i18next": "^23.11.5", + "i18next-browser-languagedetector": "^8.0.0", "mobx": "^6.9.0", "mobx-react": "^7.6.0", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-i18next": "^14.1.2", "react-router-dom": "^6.10.0", "react-to-pdf": "^1.0.1" }, diff --git a/src/hooks/usePresets.js b/src/hooks/usePresets.js new file mode 100644 index 0000000..aa046e6 --- /dev/null +++ b/src/hooks/usePresets.js @@ -0,0 +1,42 @@ +import { useEffect, useState } from 'react'; +import dayjs from "dayjs"; +import { useTranslation } from 'react-i18next'; + +const usePresets = () => { + const [presets, setPresets] = useState([]); + const { t, i18n } = useTranslation(); + + useEffect(() => { + const newPresets = [ + { + label: t("datetime.thisWeek"), + value: [dayjs().startOf("w"), dayjs().endOf("w")], + }, + { + label: t("datetime.lastWeek"), + value: [dayjs().startOf("w").subtract(7, "days"), dayjs().endOf("w").subtract(7, "days")], + }, + { + label: t("datetime.thisMonth"), + value: [dayjs().startOf("M"), dayjs().endOf("M")], + }, + { + label: t("datetime.lastMonth"), + value: [dayjs().subtract(1, "M").startOf("M"), dayjs().subtract(1, "M").endOf("M")], + }, + { + label: t("datetime.lastThreeMonth"), + value: [dayjs().subtract(2, "M").startOf("M"), dayjs().endOf("M")], + }, + { + label: t("datetime.thisYear"), + value: [dayjs().startOf("y"), dayjs().endOf("y")], + }, + ]; + setPresets(newPresets); + }, [i18n.language]); + + return presets; +} + +export default usePresets; diff --git a/src/i18n/index.jsx b/src/i18n/index.jsx new file mode 100644 index 0000000..f0b96bc --- /dev/null +++ b/src/i18n/index.jsx @@ -0,0 +1,45 @@ +import i18n from 'i18next'; +// 用于检测浏览器中的用户语言, +// https://github.com/i18next/i18next-browser-languageDetector +// 通过localStorage.getItem('i18nextLng')取出当前语言环境 +import LanguageDetector from 'i18next-browser-languagedetector'; +import { initReactI18next } from 'react-i18next'; +import en from './locales/en.json'; +import zh from './locales/zh.json'; + +i18n + .use(initReactI18next) + .use(LanguageDetector) + // https://www.i18next.com/overview/configuration-options + .init({ + // detection: { + // convertDetectedLanguage: 'Iso15897', + // convertDetectedLanguage: (lng) => lng.replace('-', '_') + // }, + supportedLngs: ['en', 'zh'], + resources: { + en: { translation: en }, + zh: { translation: zh }, + }, + fallbackLng: 'en', + // fallbackLng: (code) => { + // if (!code || code === 'en') return ['en']; + // const fallbacks = []; // [code]; + + // // add pure lang + // const langPart = code.split('-')[0]; + // if (langPart !== code) fallbacks.push(langPart); + + // fallbacks.push('en'); + // console.log('fallbacks', fallbacks); + // return fallbacks; + // }, + preload: ['en', 'zh'], + interpolation: { + escapeValue: false, + }, + // keySeparator: false, + debug: false, + }); + +export default i18n; diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json new file mode 100644 index 0000000..64c398a --- /dev/null +++ b/src/i18n/locales/en.json @@ -0,0 +1,71 @@ +{ + "lang": { + "en": "English", + "zh": "中文" + }, + "menu": { + "Reservation": "Reservation", + "Invoice": "Invoice", + "Feedback": "Feedback", + "Notice": "Notice" + }, + "loginAction": { + "ChangePassword": "Change password", + "Profile": "Profile", + "Logout": "Logout", + "ChangeVendor": "Change Vendor", + "LoginTimeout": "Login timeout", + "LoginTimeoutTip": "Please input your password" + }, + "common": { + "Search": "Search", + "Reset": "Reset", + "Cancel": "Cancel", + "Submit": "Submit", + "Confirm": "Confirm", + "Close": "Close", + "Save": "Save", + "Edit": "Edit", + "Delete": "Delete", + "Add": "Add", + "View": "View", + "Back": "Back", + "Download": "Download", + "Login": "Login" + }, + "form": { + "Username": "Username", + "Password": "Password" + }, + "datetime": { + "thisWeek": "This Week", + "lastWeek": "Last Week", + "thisMonth": "This Month", + "lastMonth": "Last Month", + "lastThreeMonth": "Last Three Month", + "thisYear": "This Year" + }, + "group": { + "ArrivalDate": "Arrival Date", + "RefNo": "Reference number", + "Pax": "Pax", + "Status": "Status", + "City": "City", + "Guide": "Guide", + "ResSendingDate": "Res. sending date", + "3DGuideTip": "Reservations without the tour guide information will be highlighted in red if the arrival date is within 3 days.", + "Attachments": "Attachments", + "ConfirmationDate": "Confirmation Date", + "ConfirmationDetails": "Confirmation Details", + + "Rate Code": "Rate Code", + "Rate Plan": "Rate Plan", + "Rate Type": "Rate Type", + "Rate Category": "Rate Category", + "Rate Class": "Rate Class", + "Rate Family": "Rate Family", + "Rate Group": "Rate Group", + "Rate Sub Group": "Rate Sub Group", + "Rate Sub Group Code": "Rate Sub Group Code" + } +} diff --git a/src/i18n/locales/zh.json b/src/i18n/locales/zh.json new file mode 100644 index 0000000..9cbd473 --- /dev/null +++ b/src/i18n/locales/zh.json @@ -0,0 +1,63 @@ +{ + "lang": { + "en": "English", + "zh": "中文" + }, + "menu": { + "Reservation": "团预订", + "Invoice": "账单", + "Feedback": "反馈表", + "Notice": "通知" + }, + "loginAction": { + "ChangePassword": "修改密码", + "Profile": "账户中心", + "Logout": "退出", + "ChangeVendor": "切换账户", + "LoginTimeout": "登录超时", + "LoginTimeoutTip": "请输入密码" + }, + "common": { + "Search": "查询", + "Reset": "重置", + "Cancel": "取消", + "Submit": "提交", + "Confirm": "确认", + "Close": "关闭", + "Save": "保存", + "Edit": "编辑", + "Delete": "删除", + "Add": "添加", + "View": "查看", + "Back": "返回", + "Download": "下载", + "Login": "登录" + }, + "form": { + "Username": "账户名", + "Password": "密码" + }, + "datetime": { + "thisWeek": "本周", + "lastWeek": "上周", + "thisMonth": "本月", + "lastMonth": "上月", + "lastThreeMonth": "前三个月", + "thisYear": "今年" + }, + "group": { + "ArrivalDate": "抵达日期", + "RefNo": "团号", + "Pax": "人数", + "Status": "状态", + "City": "城市", + "Guide": "导游", + "ResSendingDate": "发送时间", + "3DGuideTip": "红色突出显示:抵达日期在 3 天内,没有导游信息的预订。", + "Attachments": "附件", + "ConfirmationDate": "确认日期", + "ConfirmationDetails": "确认信息", + + "Rate Code": "Rate Code" + } +} diff --git a/src/main.jsx b/src/main.jsx index 21fee95..97d59e1 100644 --- a/src/main.jsx +++ b/src/main.jsx @@ -29,6 +29,7 @@ import InvoicePaid from "@/views/invoice/Paid"; import InvoicePaidDetail from "@/views/invoice/PaidDetail"; import ChangeVendor from "@/views/account/ChangeVendor"; +import './i18n'; configure({ useProxies: "ifavailable", diff --git a/src/views/App.jsx b/src/views/App.jsx index 0494b39..f91f389 100644 --- a/src/views/App.jsx +++ b/src/views/App.jsx @@ -9,58 +9,58 @@ import AppLogo from "@/assets/logo-gh.png"; import { isEmpty } from "@/utils/commons"; import { useStore } from "@/stores/StoreContext.js"; import * as config from "@/config"; +import Language from "./Language"; +import { useTranslation } from 'react-i18next'; +import i18n from '@/i18n/index'; +import zhLocale from 'antd/locale/zh_CN'; +import enLocale from 'antd/locale/en_US'; +import 'dayjs/locale/zh-cn'; const { Header, Content, Footer } = Layout; const { Title } = Typography; +// const { t } = useTranslation(); let items = []; -const items_default = [ - { - label: Change password, - key: "0", - }, - { - label: Profile, - key: "1", - }, - { - type: "divider", - }, - { - label: Logout, - key: "3", - }, -]; - -const item_manager = -[ - { - label: Change password, - key: "0", - }, - { - label: Profile, - key: "1", - }, - { - type: "divider", - }, - { - label: Logout, - key: "3", - }, - { - label:Change Vendor, - key:"4", - }, -]; - - - - +const useItemDefault = () => { + const [data, setData] = useState([]); + const { t, i18n } = useTranslation(); + + useEffect(() => { + const newData = [ + { label: {t('loginAction.ChangePassword')}, key: '0' }, + { label: {t('loginAction.Profile')}, key: '1' }, + { type: 'divider' }, + { label: {t('loginAction.Logout')}, key: '3' }, + ]; + setData(newData); + }, [i18n.language]); + + return data; +}; +const useItemManager = () => { + const items_default = useItemDefault(); + const [data, setData] = useState(items_default); + const { t, i18n } = useTranslation(); + + useEffect(() => { + const newData = [ + { label: {t('loginAction.ChangePassword')}, key: '0' }, + { label: {t('loginAction.Profile')}, key: '1' }, + { type: 'divider' }, + { label: {t('loginAction.Logout')}, key: '3' }, + { label: {t('loginAction.ChangeVendor')}, key: '4' }, + ]; + setData(newData); + }, [i18n.language]); + return data; +}; function App() { + const { t } = useTranslation(); + const items_default = useItemDefault(); + const item_manager = useItemManager(); + const [password, setPassword] = useState(''); const { authStore, noticeStore } = useStore(); const { notification } = AntApp.useApp(); @@ -124,8 +124,12 @@ function App() { token: { colorBgContainer }, } = theme.useToken(); + const [antdLng, setAntdLng] = useState(enLocale); + useEffect(() => { + setAntdLng(i18n.language === 'en' ? enLocale : zhLocale); + }, [i18n.language]); return ( - - Login timeout - Please input your password + {t('loginAction.LoginTimeout')} +
{t('loginAction.LoginTimeoutTip')}
- setPassword(e.target.value)} onPressEnter={() => onSubmit()} addonBefore={login.username} /> - + >{t('common.Submit')} + Reservation }, - { key: "invoice", label: Invoice }, - { key: "feedback", label: Feedback }, + { key: "reservation", label: {t('menu.Reservation')} }, + { key: "invoice", label: {t('menu.Invoice')} }, + { key: "feedback", label: {t('menu.Feedback')} }, // { key: "report", label: Report }, { key: "notice", label: ( - Notice + {t('menu.Notice')} {noticeUnRead ? : ""} ), @@ -188,7 +192,7 @@ function App() { {authStore.login.travelAgencyName} - + + + + { + const { t, i18n } = useTranslation(); + const [selectedKeys, setSelectedKeys] = useState([i18n.language]); + // 切换语言事件 + const handleChangeLanguage = ({ key }) => { + setSelectedKeys([key]); + i18n.changeLanguage(key); + }; + + const langSupports = ['en', 'zh'].map((lang) => ({ label: t(`lang.${lang}`), key: lang })); + + /* 🌏🌐 */ + return ( + +
🌐{t(`lang.${i18n.language}`)}
+
+ ); +}; + +export default Language; diff --git a/src/views/Login.jsx b/src/views/Login.jsx index 1dff00b..ce148d4 100644 --- a/src/views/Login.jsx +++ b/src/views/Login.jsx @@ -2,9 +2,10 @@ import { useNavigate, useLocation } from "react-router-dom"; import { useEffect } from 'react'; import { Button, Checkbox, Form, Input, Row, App } from 'antd'; import { useStore } from '@/stores/StoreContext.js'; - +import { useTranslation } from 'react-i18next'; function Login() { + const { t, i18n } = useTranslation(); const { authStore, noticeStore } = useStore(); const { notification } = App.useApp(); @@ -13,7 +14,7 @@ function Login() { const [form] = Form.useForm(); useEffect (() => { - if (location.search === '?out') { + if (location.search === '?out') { authStore.logout(); navigate('/login'); } @@ -49,7 +50,7 @@ function Login() { }); }); }; - + const onFinishFailed = (errorInfo) => { console.log('Failed:', errorInfo); }; @@ -78,7 +79,7 @@ function Login() { autoComplete="off" > @@ -116,4 +117,4 @@ function Login() { ); } -export default Login; \ No newline at end of file +export default Login; diff --git a/src/views/Standlone.jsx b/src/views/Standlone.jsx index 93333f7..f4a12ba 100644 --- a/src/views/Standlone.jsx +++ b/src/views/Standlone.jsx @@ -6,6 +6,7 @@ import { DownOutlined } from "@ant-design/icons"; import "antd/dist/reset.css"; import AppLogo from "@/assets/logo-gh.png"; import { useStore } from "@/stores/StoreContext.js"; +import Language from "./Language"; const { Title } = Typography; const { Header, Content, Footer } = Layout; @@ -35,7 +36,10 @@ function Standlone() { App logo - Global Highlights Hub + Global Highlights Hub + + + ); } - + function attachmentRender(text, confirm) { return ( <> @@ -60,10 +62,10 @@ function Detail() { ); } - + function confirmRender(text, confirm) { return ( - + ); } @@ -78,18 +80,18 @@ function Detail() { const { authStore, reservationStore } = useStore(); const { reservationDetail, confirmationList } = reservationStore; const { login } = authStore; - const officeWebViewerUrl = + const officeWebViewerUrl = 'https://view.officeapps.live.com/op/embed.aspx?wdPrint=1&wdHideGridlines=0&wdHideComments=1&wdEmbedCode=0&src='; // 测试文档:https://www.chinahighlights.com/public/reservationW220420009.doc - const reservationUrl = + const reservationUrl = `https://p9axztuwd7x8a7.mycht.cn/service-fileServer/DownloadPlanDoc?GRI_SN=${reservationId}&VEI_SN=${login.travelAgencyId}&token=${login.token}&FileType=1`; - const nameCardUrl = + const nameCardUrl = `https://p9axztuwd7x8a7.mycht.cn/service-fileServer/DownloadPlanDoc?GRI_SN=${reservationId}&VEI_SN=${login.travelAgencyId}&token=${login.token}&FileType=2`; const reservationPreviewUrl = officeWebViewerUrl + encodeURIComponent(reservationUrl); const nameCardPreviewUrl = officeWebViewerUrl + encodeURIComponent(nameCardUrl); - const showConfirmModal = (confirm) => { + const showConfirmModal = (confirm) => { setIsModalOpen(true); const formattedText = confirm.PCI_ConfirmText;//.replace(/\;/g, "\n——————————————————————\n"); setConfirmText(formattedText); @@ -153,31 +155,31 @@ function Detail() { - Reference Number: {reservationDetail.referenceNumber}; Arrival date: {reservationDetail.arrivalDate}; + {t('group.RefNo')}: {reservationDetail.referenceNumber}; {t('group.ArrivalDate')}: {reservationDetail.arrivalDate}; - + - + - + - - + diff --git a/src/views/reservation/Newest.jsx b/src/views/reservation/Newest.jsx index 57d471f..83c6196 100644 --- a/src/views/reservation/Newest.jsx +++ b/src/views/reservation/Newest.jsx @@ -7,13 +7,17 @@ import dayjs from "dayjs"; import { useStore } from '@/stores/StoreContext.js'; import { DATE_PRESETS } from "@/config"; import { formatDate, isEmpty } from "@/utils/commons"; +import { useTranslation } from 'react-i18next'; +import usePresets from '@/hooks/usePresets'; const { Title } = Typography; function Newest() { + const { t } = useTranslation(); + const presets = usePresets(); const reservationListColumns = [ { - title: 'Reference number', + title: t('group.RefNo'), dataIndex: 'referenceNumber', key: 'Reference number', render: (text, record) => { @@ -28,47 +32,47 @@ function Newest() { }, }, { - title: 'Arrival date', + title: t('group.ArrivalDate'), dataIndex: 'arrivalDate', key: 'Arrival date', render: (text, record) => (isEmpty(text) ? '' : dayjs(text).format('YYYY-MM-DD')), }, { - title: 'Pax', + title: t('group.Pax'), key: 'Pax', dataIndex: 'pax' }, { - title: 'Status', + title: t('group.Status'), key: 'Status', dataIndex: 'status' }, { - title: 'Res. sending date', + title: t('group.ResSendingDate'), key: 'Reservation date', dataIndex: 'reservationDate', render: (text, record) => (isEmpty(text) ? '' : dayjs(text).format('YYYY-MM-DD')), }, { - title: 'Guide', + title: t('group.Guide'), key: 'Guide', dataIndex: 'guide', render: guideRender }, ]; - + function guideRender(text, reservation) { if (reservation.guide === '') { return ( - + ); } else { return ( {reservation.guide} - + ); } @@ -110,7 +114,7 @@ function Newest() { useEffect (() => { if (location.search !== '?back') { - // 第一页,未确认计划 + // 第一页,未确认计划 onSearchClick(1, 1); } reservationStore.fetchAllGuideList() @@ -129,7 +133,7 @@ function Newest() { }, []); const showCityGuideModal = (reservation) => { - setDataLoading(true); + setDataLoading(true); setIsModalOpen(true); reservationStore.editReservation(reservation); reservationStore.fetchCityList(reservation.reservationId) @@ -156,7 +160,7 @@ function Newest() { setIsModalOpen(false); setDataLoading(false); }; - + // 默认重新搜索第一页,所有状态的计划 const onSearchClick = (current=1, status=null) => { setDataLoading(true); @@ -189,12 +193,12 @@ function Newest() { pagination={false} columns={[ { - title: 'City', + title: t('group.City'), dataIndex: 'cityName', key: 'cityName' }, { - title: 'Tour Guide', + title: t('group.Guide'), dataIndex: 'tourGuide', key: 'tourGuide', render: cityGuideRender, @@ -210,32 +214,32 @@ function Newest() { - { reservationStore.updatePropertyValue('referenceNo', e.target.value)} } /> + { reservationStore.updatePropertyValue('referenceNo', e.target.value)} } /> - Arrival Date + {t('group.ArrivalDate')} { + onChange={(dateRange) => { reservationStore.updatePropertyValue('arrivalDateRange', dateRange == null ? [] : dateRange) }} /> - +
'Reservations without the tour guide information will be highlighted in red if the arrival date is within 3 days.'} + title={() => t('group.3DGuideTip')} bordered loading={dataLoading} pagination={{ @@ -255,4 +259,4 @@ function Newest() { ); } -export default observer(Newest); \ No newline at end of file +export default observer(Newest);