Add Chinese translation (Thank you copilot, and I swear I reviewed the code carefully)

pull/169/head
lyc8503 2025-12-23 22:57:02 +08:00
parent 77cdf19f7d
commit e9d8a1c842
16 changed files with 324 additions and 68 deletions

View File

@ -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')
) : (
<>
<div>{dayPercent + '% at ' + new Date(dayStart * 1000).toLocaleDateString()}</div>
<div>
{t('percent at date', {
percent: dayPercent,
date: new Date(dayStart * 1000).toLocaleDateString(),
})}
</div>
{dayDownTime > 0 && (
<div>{`Down for ${moment.preciseDiff(
moment(0),
moment(dayDownTime * 1000)
)} (click for detail)`}</div>
<div>
{t('Down for', {
duration: moment.preciseDiff(moment(0), moment(dayDownTime * 1000)),
})}
</div>
)}
</>
)
@ -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(
<>

View File

@ -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,
},
},

View File

@ -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 (
<a
@ -18,7 +20,7 @@ export default function Header({ style }: { style?: React.CSSProperties }) {
)
}
const links = [{ label: 'Incidents', link: '/incidents' }, ...(pageConfig.links || [])]
const links = [{ label: t('Incidents'), link: '/incidents' }, ...(pageConfig.links || [])]
return (
<header className={classes.header} style={style}>

View File

@ -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'))}
</span>
}
color={
@ -61,14 +63,14 @@ export default function MaintenanceAlert({
}}
>
<div style={{ textAlign: 'right', fontWeight: 'bold' }}>
{upcoming ? 'Scheduled for:' : 'From:'}
{upcoming ? t('Scheduled for') : t('From')}
</div>
<div>{new Date(maintenance.start).toLocaleString()}</div>
<div style={{ textAlign: 'right', fontWeight: 'bold' }}>
{upcoming ? 'Expected end:' : 'To:'}
{upcoming ? t('Expected end') : t('To')}
</div>
<div>
{maintenance.end ? new Date(maintenance.end).toLocaleString() : 'Until further notice'}
{maintenance.end ? new Date(maintenance.end).toLocaleString() : t('Until further notice')}
</div>
</div>
</div>
@ -77,11 +79,11 @@ export default function MaintenanceAlert({
{maintenance.monitors && maintenance.monitors.length > 0 && (
<>
<Text mt="xs">
<b>Affected components:</b>
<b>{t('Affected components')}</b>
</Text>
<List size="sm" withPadding>
{maintenance.monitors.map((comp, compIdx) => (
<List.Item key={compIdx}>{comp?.name ?? '[ERR: MONITOR ID NOT FOUND]'}</List.Item>
<List.Item key={compIdx}>{comp?.name ?? t('MONITOR ID NOT FOUND')}</List.Item>
))}
</List>
</>

View File

@ -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}
</Text>
<Text mt="sm" fw={700}>
No data available, please make sure you have deployed your workers with latest config and
check your worker status!
{t('No data available')}
</Text>
</>
)
@ -91,7 +93,7 @@ export default function MonitorDetail({
)}
<Text mt="sm" fw={700} style={{ display: 'inline', color: getColor(uptimePercent, true) }}>
Overall: {uptimePercent}%
{t('Overall', { percent: uptimePercent })}
</Text>
</div>

View File

@ -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')}
</Text>
</div>
</Accordion.Control>

View File

@ -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 (
<Alert
icon={<IconInfoCircle />}
@ -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')}
</span>
}
color="gray"
@ -23,7 +25,7 @@ export default function NoIncidentsAlert({ style }: { style?: React.CSSPropertie
...style,
}}
>
<Text>There are no incidents for this month.</Text>
<Text>{t('There are no incidents for this month')}</Text>
</Alert>
)
}

View File

@ -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 = <IconAlertCircle style={{ width: 64, height: 64, color: '#b91c1c' }} />
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 = <IconCircleCheck style={{ width: 64, height: 64, color: '#059669' }} />
} 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}
</Title>
<Title mt="sm" style={{ textAlign: 'center', color: '#70778c' }} order={5}>
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,
})}
</Title>
{/* Upcoming Maintenance */}
{upcomingMaintenances.length > 0 && (
<>
<Title mt="4px" style={{ textAlign: 'center', color: '#70778c' }} order={5}>
{`${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>
</Title>

37
locales/en/common.json Normal file
View File

@ -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 →"
}

37
locales/zh-CN/common.json Normal file
View File

@ -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": "下一月 →"
}

176
package-lock.json generated
View File

@ -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",

View File

@ -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",

View File

@ -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 (

View File

@ -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<string | null>('')
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() {
<Container size="md" style={{ width: '100%' }}>
<Group justify="end" mb="md">
<Select
placeholder="Select monitor"
placeholder={t('Select monitor')}
data={monitorOptions}
value={selectedMonitor}
onChange={setSelectedMonitor}
@ -113,13 +115,13 @@ export default function IncidentsPage() {
</Box>
<Group justify="space-between" mt="md">
<Button variant="default" onClick={() => (window.location.hash = prev)}>
Backwards
{t('Backwards')}
</Button>
<Box style={{ alignSelf: 'center', fontWeight: 500, fontSize: 18 }}>
{selectedMonth}
</Box>
<Button variant="default" onClick={() => (window.location.hash = next)}>
Forward
{t('Forward')}
</Button>
</Group>
</Container>

View File

@ -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 <Text fw={700}>Monitor with id {monitorId} not found!</Text>
return <Text fw={700}>{t('Monitor not found', { id: monitorId })}</Text>
}
return (
<div style={{ maxWidth: '810px' }}>
@ -55,8 +57,7 @@ export default function Home({
{state == undefined ? (
<Center>
<Text fw={700}>
Monitor State is not defined now, please check your worker&apos;s status and KV
binding!
{t('Monitor State not defined')}
</Text>
</Center>
) : (

25
util/i18n.ts Normal file
View File

@ -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;