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')
) : (
<>
-
+ {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 })}{' '}
setExpandUpcoming(!expandUpcoming)}
>
- {expandUpcoming ? '[Hide]' : '[Show]'}
+ {expandUpcoming ? t('Hide') : t('Show')}
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() {
diff --git a/pages/index.tsx b/pages/index.tsx
index d4fb9b6..ee6dc09 100644
--- a/pages/index.tsx
+++ b/pages/index.tsx
@@ -10,6 +10,7 @@ import MonitorList from '@/components/MonitorList'
import { Center, Text } from '@mantine/core'
import MonitorDetail from '@/components/MonitorDetail'
import Footer from '@/components/Footer'
+import { useTranslation } from 'react-i18next'
export const runtime = 'experimental-edge'
const inter = Inter({ subsets: ['latin'] })
@@ -23,6 +24,7 @@ export default function Home({
tooltip?: string
statusPageLink?: string
}) {
+ const { t } = useTranslation('common')
let state
if (stateStr !== undefined) {
state = JSON.parse(stateStr) as MonitorState
@@ -33,7 +35,7 @@ export default function Home({
if (monitorId) {
const monitor = monitors.find((monitor) => monitor.id === monitorId)
if (!monitor || !state) {
- return Monitor with id {monitorId} not found!
+ return {t('Monitor not found', { id: monitorId })}
}
return (
@@ -55,8 +57,7 @@ export default function Home({
{state == undefined ? (
- Monitor State is not defined now, please check your worker's status and KV
- binding!
+ {t('Monitor State not defined')}
) : (
diff --git a/util/i18n.ts b/util/i18n.ts
new file mode 100644
index 0000000..d3ed910
--- /dev/null
+++ b/util/i18n.ts
@@ -0,0 +1,25 @@
+import i18n from 'i18next';
+import { initReactI18next } from 'react-i18next';
+import LanguageDetector from 'i18next-browser-languagedetector';
+import en from '../locales/en/common.json';
+import zhCN from '../locales/zh-CN/common.json';
+
+i18n
+ .use(LanguageDetector)
+ .use(initReactI18next)
+ .init({
+ resources: {
+ en: { common: en },
+ 'zh-CN': { common: zhCN },
+ zh: { common: zhCN },
+ },
+ fallbackLng: 'en',
+ interpolation: {
+ escapeValue: false,
+ },
+ detection: {
+ order: ['navigator'],
+ },
+ });
+
+export default i18n;