diff --git a/components/DetailBar.tsx b/components/DetailBar.tsx index d336fcc..f8bb98c 100644 --- a/components/DetailBar.tsx +++ b/components/DetailBar.tsx @@ -3,6 +3,7 @@ import { getColor } from '@/util/color' import { Box, Tooltip, Modal } from '@mantine/core' import { useResizeObserver } from '@mantine/hooks' import { useState } from 'react' +import { useTranslation } from 'react-i18next' const moment = require('moment') require('moment-precise-range-plugin') @@ -13,6 +14,7 @@ export default function DetailBar({ monitor: MonitorTarget state: MonitorState }) { + const { t } = useTranslation('common') const [barRef, barRect] = useResizeObserver() const [modalOpened, setModalOpened] = useState(false) const [modalTitle, setModalTitle] = useState('') @@ -79,15 +81,21 @@ export default function DetailBar({ events={{ hover: true, focus: false, touch: true }} label={ Number.isNaN(Number(dayPercent)) ? ( - 'No Data' + t('No Data') ) : ( <> -
{dayPercent + '% at ' + new Date(dayStart * 1000).toLocaleDateString()}
+
+ {t('percent at date', { + percent: dayPercent, + date: new Date(dayStart * 1000).toLocaleDateString(), + })} +
{dayDownTime > 0 && ( -
{`Down for ${moment.preciseDiff( - moment(0), - moment(dayDownTime * 1000) - )} (click for detail)`}
+
+ {t('Down for', { + duration: moment.preciseDiff(moment(0), moment(dayDownTime * 1000)), + })} +
)} ) @@ -105,7 +113,10 @@ export default function DetailBar({ onClick={() => { if (dayDownTime > 0) { setModalTitle( - `🚨 ${monitor.name} incidents at ${new Date(dayStart * 1000).toLocaleDateString()}` + t('incidents at', { + name: monitor.name, + date: new Date(dayStart * 1000).toLocaleDateString(), + }) ) setModelContent( <> diff --git a/components/DetailChart.tsx b/components/DetailChart.tsx index ec4981f..bd50894 100644 --- a/components/DetailChart.tsx +++ b/components/DetailChart.tsx @@ -13,6 +13,7 @@ import { import 'chartjs-adapter-moment' import { MonitorState, MonitorTarget } from '@/types/config' import { codeToCountry } from '@/util/iata' +import { useTranslation } from 'react-i18next' ChartJS.register( CategoryScale, @@ -32,6 +33,7 @@ export default function DetailChart({ monitor: MonitorTarget state: MonitorState }) { + const { t } = useTranslation('common') const latencyData = state.latency[monitor.id].recent.map((point) => ({ x: point.time * 1000, y: point.ping, @@ -76,7 +78,7 @@ export default function DetailChart({ }, title: { display: true, - text: 'Response times(ms)', + text: t('Response times'), align: 'start' as const, }, }, diff --git a/components/Header.tsx b/components/Header.tsx index 712dc75..bfccf32 100644 --- a/components/Header.tsx +++ b/components/Header.tsx @@ -2,8 +2,10 @@ import { Container, Group, Image } from '@mantine/core' import classes from '@/styles/Header.module.css' import { pageConfig } from '@/uptime.config' import { PageConfigLink } from '@/types/config' +import { useTranslation } from 'react-i18next' export default function Header({ style }: { style?: React.CSSProperties }) { + const { t } = useTranslation('common') const linkToElement = (link: PageConfigLink, i: number) => { return ( diff --git a/components/MaintenanceAlert.tsx b/components/MaintenanceAlert.tsx index 8d76fd9..b460046 100644 --- a/components/MaintenanceAlert.tsx +++ b/components/MaintenanceAlert.tsx @@ -3,6 +3,7 @@ import { useMediaQuery } from '@mantine/hooks' import { IconAlertTriangle } from '@tabler/icons-react' import { MaintenanceConfig, MonitorTarget } from '@/types/config' import { pageConfig } from '@/uptime.config' +import { useTranslation } from 'react-i18next' export default function MaintenanceAlert({ maintenance, @@ -13,6 +14,7 @@ export default function MaintenanceAlert({ style?: React.CSSProperties upcoming?: boolean }) { + const { t } = useTranslation('common') const theme = useMantineTheme() const isDesktop = useMediaQuery(`(min-width: ${theme.breakpoints.sm})`) @@ -26,7 +28,7 @@ export default function MaintenanceAlert({ fontWeight: 700, }} > - {(upcoming ? '[Upcoming] ' : '') + (maintenance.title || 'Scheduled Maintenance')} + {(upcoming ? t('Upcoming') : '') + (maintenance.title || t('Scheduled Maintenance'))} } color={ @@ -61,14 +63,14 @@ export default function MaintenanceAlert({ }} >
- {upcoming ? 'Scheduled for:' : 'From:'} + {upcoming ? t('Scheduled for') : t('From')}
{new Date(maintenance.start).toLocaleString()}
- {upcoming ? 'Expected end:' : 'To:'} + {upcoming ? t('Expected end') : t('To')}
- {maintenance.end ? new Date(maintenance.end).toLocaleString() : 'Until further notice'} + {maintenance.end ? new Date(maintenance.end).toLocaleString() : t('Until further notice')}
@@ -77,11 +79,11 @@ export default function MaintenanceAlert({ {maintenance.monitors && maintenance.monitors.length > 0 && ( <> - Affected components: + {t('Affected components')} {maintenance.monitors.map((comp, compIdx) => ( - {comp?.name ?? '[ERR: MONITOR ID NOT FOUND]'} + {comp?.name ?? t('MONITOR ID NOT FOUND')} ))} diff --git a/components/MonitorDetail.tsx b/components/MonitorDetail.tsx index 66f95c6..c2bb67f 100644 --- a/components/MonitorDetail.tsx +++ b/components/MonitorDetail.tsx @@ -5,6 +5,7 @@ import DetailChart from './DetailChart' import DetailBar from './DetailBar' import { getColor } from '@/util/color' import { maintenances } from '@/uptime.config' +import { useTranslation } from 'react-i18next' export default function MonitorDetail({ monitor, @@ -13,6 +14,8 @@ export default function MonitorDetail({ monitor: MonitorTarget state: MonitorState }) { + const { t } = useTranslation('common') + if (!state.latency[monitor.id]) return ( <> @@ -20,8 +23,7 @@ export default function MonitorDetail({ {monitor.name} - No data available, please make sure you have deployed your workers with latest config and - check your worker status! + {t('No data available')} ) @@ -91,7 +93,7 @@ export default function MonitorDetail({ )} - Overall: {uptimePercent}% + {t('Overall', { percent: uptimePercent })} diff --git a/components/MonitorList.tsx b/components/MonitorList.tsx index 610caf0..6444d5c 100644 --- a/components/MonitorList.tsx +++ b/components/MonitorList.tsx @@ -3,6 +3,7 @@ import { Accordion, Card, Center, Text } from '@mantine/core' import MonitorDetail from './MonitorDetail' import { pageConfig } from '@/uptime.config' import { useEffect, useState } from 'react' +import { useTranslation } from 'react-i18next' function countDownCount(state: MonitorState, ids: string[]) { let downCount = 0 @@ -36,6 +37,7 @@ export default function MonitorList({ monitors: MonitorTarget[] state: MonitorState }) { + const { t } = useTranslation('common') const group = pageConfig.group const groupedMonitor = group && Object.keys(group).length > 0 let content @@ -81,7 +83,7 @@ export default function MonitorList({ }} > {group[groupName].length - countDownCount(state, group[groupName])}/ - {group[groupName].length} Operational + {group[groupName].length} {t('Operational')} diff --git a/components/NoIncidents.tsx b/components/NoIncidents.tsx index f3b8066..865281b 100644 --- a/components/NoIncidents.tsx +++ b/components/NoIncidents.tsx @@ -1,7 +1,9 @@ import { Alert, Text } from '@mantine/core' import { IconInfoCircle } from '@tabler/icons-react' +import { useTranslation } from 'react-i18next' export default function NoIncidentsAlert({ style }: { style?: React.CSSProperties }) { + const { t } = useTranslation('common') return ( } @@ -12,7 +14,7 @@ export default function NoIncidentsAlert({ style }: { style?: React.CSSPropertie fontWeight: 700, }} > - {'No incidents in this month'} + {t('No incidents in this month')} } color="gray" @@ -23,7 +25,7 @@ export default function NoIncidentsAlert({ style }: { style?: React.CSSPropertie ...style, }} > - There are no incidents for this month. + {t('There are no incidents for this month')} ) } diff --git a/components/OverallStatus.tsx b/components/OverallStatus.tsx index 08ec053..40f57c7 100644 --- a/components/OverallStatus.tsx +++ b/components/OverallStatus.tsx @@ -4,6 +4,7 @@ import { IconCircleCheck, IconAlertCircle, IconPlus, IconMinus } from '@tabler/i import { useEffect, useState } from 'react' import MaintenanceAlert from './MaintenanceAlert' import { pageConfig } from '@/uptime.config' +import { useTranslation } from 'react-i18next' function useWindowVisibility() { const [isVisible, setIsVisible] = useState(true) @@ -24,22 +25,24 @@ export default function OverallStatus({ maintenances: MaintenanceConfig[] monitors: MonitorTarget[] }) { + const { t } = useTranslation('common') let group = pageConfig.group let groupedMonitor = (group && Object.keys(group).length > 0) || false let statusString = '' let icon = if (state.overallUp === 0 && state.overallDown === 0) { - statusString = 'No data yet' + statusString = t('No data yet') } else if (state.overallUp === 0) { - statusString = 'All systems not operational' + statusString = t('All systems not operational') } else if (state.overallDown === 0) { - statusString = 'All systems operational' + statusString = t('All systems operational') icon = } else { - statusString = `Some systems not operational (${state.overallDown} out of ${ - state.overallUp + state.overallDown - })` + statusString = t('Some systems not operational', { + down: state.overallDown, + total: state.overallUp + state.overallDown, + }) } const [openTime] = useState(Math.round(Date.now() / 1000)) @@ -89,24 +92,22 @@ export default function OverallStatus({ {statusString} - Last updated on:{' '} - {`${new Date(state.lastUpdate * 1000).toLocaleString()} (${ - currentTime - state.lastUpdate - } sec ago)`} + {t('Last updated on', { + date: new Date(state.lastUpdate * 1000).toLocaleString(), + seconds: currentTime - state.lastUpdate, + })} {/* Upcoming Maintenance */} {upcomingMaintenances.length > 0 && ( <> - {`${upcomingMaintenances.length} upcoming ${ - upcomingMaintenances.length === 1 ? 'maintenance' : 'maintenances' - }`}{' '} + {t('upcoming maintenance', { count: upcomingMaintenances.length })}{' '} <span style={{ textDecoration: 'underline', cursor: 'pointer' }} onClick={() => setExpandUpcoming(!expandUpcoming)} > - {expandUpcoming ? '[Hide]' : '[Show]'} + {expandUpcoming ? t('Hide') : t('Show')} </span> diff --git a/locales/en/common.json b/locales/en/common.json new file mode 100644 index 0000000..137c557 --- /dev/null +++ b/locales/en/common.json @@ -0,0 +1,37 @@ +{ + "Incidents": "Incidents", + "No data yet": "No data yet", + "All systems not operational": "All systems not operational", + "All systems operational": "All systems operational", + "Some systems not operational": "Some systems not operational ({{down}} out of {{total}})", + "Last updated on": "Last updated on: {{date}} ({{seconds}} sec ago)", + "upcoming maintenance": "{{count}} upcoming maintenance", + "upcoming maintenance_plural": "{{count}} upcoming maintenances", + "Hide": "[Hide]", + "Show": "[Show]", + "Operational": "Operational", + "No data available": "No data available, please make sure you have deployed your workers with latest config and check your worker status!", + "Overall": "Overall: {{percent}}%", + "No Data": "No Data", + "percent at date": "{{percent}}% at {{date}}", + "Down for": "Down for {{duration}} (click for detail)", + "incidents at": "🚨 {{name}} incidents at {{date}}", + "Response times": "Response times(ms)", + "Upcoming": "[Upcoming] ", + "Scheduled Maintenance": "Scheduled Maintenance", + "Scheduled for": "Scheduled for:", + "From": "From:", + "Expected end": "Expected end:", + "To": "To:", + "Until further notice": "Until further notice", + "Affected components": "Affected components:", + "MONITOR ID NOT FOUND": "[ERR: MONITOR ID NOT FOUND]", + "No incidents in this month": "No incidents in this month", + "There are no incidents for this month": "There are no incidents for this month.", + "Monitor not found": "Monitor with id {{id}} not found!", + "Monitor State not defined": "Monitor State is not defined now, please check your worker's status and KV binding!", + "All": "All", + "Select monitor": "Select monitor", + "Backwards": "← Backwards", + "Forward": "Forward →" +} diff --git a/locales/zh-CN/common.json b/locales/zh-CN/common.json new file mode 100644 index 0000000..01ea202 --- /dev/null +++ b/locales/zh-CN/common.json @@ -0,0 +1,37 @@ +{ + "Incidents": "历史故障", + "No data yet": "暂无数据", + "All systems not operational": "系统均离线", + "All systems operational": "系统一切正常", + "Some systems not operational": "部分系统离线 ({{down}} / {{total}})", + "Last updated on": "最后更新于: {{date}} ({{seconds}} 秒前)", + "upcoming maintenance": "{{count}} 个即将进行的维护", + "upcoming maintenance_plural": "{{count}} 个即将进行的维护", + "Hide": "[隐藏]", + "Show": "[显示]", + "Operational": "在线", + "No data available": "暂无数据,请确保您已使用最新配置部署了 Worker 并检查 Worker 状态!", + "Overall": "总可用率: {{percent}}%", + "No Data": "暂无数据", + "percent at date": "{{percent}}% 于 {{date}}", + "Down for": "故障持续 {{duration}} (点击查看详情)", + "incidents at": "🚨 {{name}} 于 {{date}} 不可用", + "Response times": "响应时间(ms)", + "Upcoming": "[即将开始] ", + "Scheduled Maintenance": "计划维护", + "Scheduled for": "计划开始:", + "From": "开始:", + "Expected end": "预计结束:", + "To": "结束:", + "Until further notice": "直到另行通知", + "Affected components": "受影响组件:", + "MONITOR ID NOT FOUND": "[错误: 未找到监控 ID]", + "No incidents in this month": "本月无故障", + "There are no incidents for this month": "本月没有发生任何故障。", + "Monitor not found": "未找到 ID 为 {{id}} 的监控!", + "Monitor State not defined": "监控状态未定义,请检查您的 Worker 状态和 KV 绑定!", + "All": "全部", + "Select monitor": "选择监控", + "Backwards": "← 上一月", + "Forward": "下一月 →" +} diff --git a/package-lock.json b/package-lock.json index cde40c4..d3f7ad7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,12 +16,15 @@ "@types/moment-precise-range-plugin": "^0.2.2", "chart.js": "^4.4.0", "chartjs-adapter-moment": "^1.0.1", + "i18next": "^25.7.3", + "i18next-browser-languagedetector": "^8.2.0", "moment": "^2.29.4", "moment-precise-range-plugin": "^1.3.0", "next": "^14.2.28", "react": "^18.3.1", "react-chartjs-2": "^5.2.0", - "react-dom": "^18.3.1" + "react-dom": "^18.3.1", + "react-i18next": "^16.5.0" }, "devDependencies": { "@cloudflare/next-on-pages": "^1.13.12", @@ -48,13 +51,10 @@ } }, "node_modules/@babel/runtime": { - "version": "7.27.0", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.0.tgz", - "integrity": "sha512-VtPOkrdPHZsKc/clNqyi9WUA8TINkZ4cGk63UUE3u4pmB2k+ZMQRDuIOagv8UVd6j7k0T3+RRIb7beKTebNbcw==", + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz", + "integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==", "license": "MIT", - "dependencies": { - "regenerator-runtime": "^0.14.0" - }, "engines": { "node": ">=6.9.0" } @@ -6035,6 +6035,15 @@ "node": ">=12.0.0" } }, + "node_modules/html-parse-stringify": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz", + "integrity": "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==", + "license": "MIT", + "dependencies": { + "void-elements": "3.1.0" + } + }, "node_modules/http-errors": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.4.0.tgz", @@ -6073,6 +6082,46 @@ "node": ">= 14" } }, + "node_modules/i18next": { + "version": "25.7.3", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-25.7.3.tgz", + "integrity": "sha512-2XaT+HpYGuc2uTExq9TVRhLsso+Dxym6PWaKpn36wfBmTI779OQ7iP/XaZHzrnGyzU4SHpFrTYLKfVyBfAhVNA==", + "funding": [ + { + "type": "individual", + "url": "https://locize.com" + }, + { + "type": "individual", + "url": "https://locize.com/i18next.html" + }, + { + "type": "individual", + "url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project" + } + ], + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.4" + }, + "peerDependencies": { + "typescript": "^5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/i18next-browser-languagedetector": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/i18next-browser-languagedetector/-/i18next-browser-languagedetector-8.2.0.tgz", + "integrity": "sha512-P+3zEKLnOF0qmiesW383vsLdtQVyKtCNA9cjSoKCppTKPQVfKd2W8hbVo5ZhNJKDqeM7BOcvNoKJOjpHh4Js9g==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.23.2" + } + }, "node_modules/iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", @@ -7752,6 +7801,33 @@ "react": "^18.3.1" } }, + "node_modules/react-i18next": { + "version": "16.5.0", + "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-16.5.0.tgz", + "integrity": "sha512-IMpPTyCTKxEj8klCrLKUTIUa8uYTd851+jcu2fJuUB9Agkk9Qq8asw4omyeHVnOXHrLgQJGTm5zTvn8HpaPiqw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.27.6", + "html-parse-stringify": "^3.0.1", + "use-sync-external-store": "^1.6.0" + }, + "peerDependencies": { + "i18next": ">= 25.6.2", + "react": ">= 16.8.0", + "typescript": "^5" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, "node_modules/react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", @@ -7884,11 +7960,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/regenerator-runtime": { - "version": "0.14.0", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.0.tgz", - "integrity": "sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA==" - }, "node_modules/regexp.prototype.flags": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.1.tgz", @@ -8758,7 +8829,7 @@ "version": "5.2.2", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz", "integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==", - "dev": true, + "devOptional": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -8939,6 +9010,15 @@ } } }, + "node_modules/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -9027,6 +9107,15 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/void-elements": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz", + "integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/web-vitals": { "version": "0.2.4", "resolved": "https://registry.npmjs.org/web-vitals/-/web-vitals-0.2.4.tgz", @@ -9608,12 +9697,9 @@ "dev": true }, "@babel/runtime": { - "version": "7.27.0", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.0.tgz", - "integrity": "sha512-VtPOkrdPHZsKc/clNqyi9WUA8TINkZ4cGk63UUE3u4pmB2k+ZMQRDuIOagv8UVd6j7k0T3+RRIb7beKTebNbcw==", - "requires": { - "regenerator-runtime": "^0.14.0" - } + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz", + "integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==" }, "@cloudflare/kv-asset-handler": { "version": "0.4.0", @@ -13361,6 +13447,14 @@ "integrity": "sha512-fJ7cW7fQGCYAkgv4CPfwFHrfd/cLS4Hau96JuJ+ZTOWhjnhoeN1ub1tFmALm/+lW5z4WCAuAV9bm05AP0mS6Gw==", "peer": true }, + "html-parse-stringify": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz", + "integrity": "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==", + "requires": { + "void-elements": "3.1.0" + } + }, "http-errors": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.4.0.tgz", @@ -13392,6 +13486,22 @@ "debug": "4" } }, + "i18next": { + "version": "25.7.3", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-25.7.3.tgz", + "integrity": "sha512-2XaT+HpYGuc2uTExq9TVRhLsso+Dxym6PWaKpn36wfBmTI779OQ7iP/XaZHzrnGyzU4SHpFrTYLKfVyBfAhVNA==", + "requires": { + "@babel/runtime": "^7.28.4" + } + }, + "i18next-browser-languagedetector": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/i18next-browser-languagedetector/-/i18next-browser-languagedetector-8.2.0.tgz", + "integrity": "sha512-P+3zEKLnOF0qmiesW383vsLdtQVyKtCNA9cjSoKCppTKPQVfKd2W8hbVo5ZhNJKDqeM7BOcvNoKJOjpHh4Js9g==", + "requires": { + "@babel/runtime": "^7.23.2" + } + }, "iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", @@ -14549,6 +14659,16 @@ "scheduler": "^0.23.2" } }, + "react-i18next": { + "version": "16.5.0", + "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-16.5.0.tgz", + "integrity": "sha512-IMpPTyCTKxEj8klCrLKUTIUa8uYTd851+jcu2fJuUB9Agkk9Qq8asw4omyeHVnOXHrLgQJGTm5zTvn8HpaPiqw==", + "requires": { + "@babel/runtime": "^7.27.6", + "html-parse-stringify": "^3.0.1", + "use-sync-external-store": "^1.6.0" + } + }, "react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", @@ -14626,11 +14746,6 @@ "which-builtin-type": "^1.1.3" } }, - "regenerator-runtime": { - "version": "0.14.0", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.0.tgz", - "integrity": "sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA==" - }, "regexp.prototype.flags": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.1.tgz", @@ -15249,7 +15364,7 @@ "version": "5.2.2", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz", "integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==", - "dev": true + "devOptional": true }, "ufo": { "version": "1.6.1", @@ -15364,6 +15479,12 @@ "tslib": "^2.0.0" } }, + "use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "requires": {} + }, "util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -15425,6 +15546,11 @@ } } }, + "void-elements": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz", + "integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==" + }, "web-vitals": { "version": "0.2.4", "resolved": "https://registry.npmjs.org/web-vitals/-/web-vitals-0.2.4.tgz", diff --git a/package.json b/package.json index 3ad502e..9f59458 100644 --- a/package.json +++ b/package.json @@ -18,12 +18,15 @@ "@types/moment-precise-range-plugin": "^0.2.2", "chart.js": "^4.4.0", "chartjs-adapter-moment": "^1.0.1", + "i18next": "^25.7.3", + "i18next-browser-languagedetector": "^8.2.0", "moment": "^2.29.4", "moment-precise-range-plugin": "^1.3.0", "next": "^14.2.28", "react": "^18.3.1", "react-chartjs-2": "^5.2.0", - "react-dom": "^18.3.1" + "react-dom": "^18.3.1", + "react-i18next": "^16.5.0" }, "devDependencies": { "@cloudflare/next-on-pages": "^1.13.12", diff --git a/pages/_app.tsx b/pages/_app.tsx index aceade1..1521da4 100644 --- a/pages/_app.tsx +++ b/pages/_app.tsx @@ -2,6 +2,7 @@ import '@mantine/core/styles.css' import type { AppProps } from 'next/app' import { MantineProvider } from '@mantine/core' import NoSsr from '@/components/NoSsr' +import '@/util/i18n' export default function App({ Component, pageProps }: AppProps) { return ( diff --git a/pages/incidents.tsx b/pages/incidents.tsx index 1838949..9cb5394 100644 --- a/pages/incidents.tsx +++ b/pages/incidents.tsx @@ -9,6 +9,7 @@ import Footer from '@/components/Footer' import { useEffect, useState } from 'react' import MaintenanceAlert from '@/components/MaintenanceAlert' import NoIncidentsAlert from '@/components/NoIncidents' +import { useTranslation } from 'react-i18next' export const runtime = 'experimental-edge' const inter = Inter({ subsets: ['latin'] }) @@ -53,6 +54,7 @@ function getPrevNextMonth(monthStr: string) { } export default function IncidentsPage() { + const { t } = useTranslation('common') const [selectedMonitor, setSelectedMonitor] = useState('') const [selectedMonth, setSelectedMonth] = useState(getSelectedMonth()) @@ -70,7 +72,7 @@ export default function IncidentsPage() { const { prev, next } = getPrevNextMonth(selectedMonth) const monitorOptions = [ - { value: '', label: 'All' }, + { value: '', label: t('All') }, ...workerConfig.monitors.map((monitor) => ({ value: monitor.id, label: monitor.name, @@ -94,7 +96,7 @@ export default function IncidentsPage() {