feat: add i18n support #3

This commit is contained in:
moonrailgun 2024-02-12 00:10:38 +08:00
parent 3d9921f16f
commit bf6b121041
67 changed files with 2588 additions and 326 deletions

File diff suppressed because it is too large Load Diff

View File

@ -6,6 +6,7 @@ import { DateRange, useGlobalStateStore } from '../store/global';
import { compact } from 'lodash-es';
import clsx from 'clsx';
import { useGlobalRangeDate } from '../hooks/useGlobalRangeDate';
import { useTranslation } from '@i18next-toolkit/react';
const { RangePicker } = DatePicker;
@ -13,6 +14,7 @@ interface DateFilterProps {
className?: string;
}
export const DateFilter: React.FC<DateFilterProps> = React.memo((props) => {
const { t } = useTranslation();
const [showPicker, setShowPicker] = useState(false);
const [showDropdown, setShowDropdown] = useState(false);
@ -26,19 +28,19 @@ export const DateFilter: React.FC<DateFilterProps> = React.memo((props) => {
},
items: compact([
{
label: 'Today',
label: t('Today'),
onClick: () => {
useGlobalStateStore.setState({ dateRange: DateRange.Today });
},
},
{
label: 'Last 24 Hours',
label: t('Last 24 Hours'),
onClick: () => {
useGlobalStateStore.setState({ dateRange: DateRange.Last24Hours });
},
},
{
label: 'Yesterday',
label: t('Yesterday'),
onClick: () => {
useGlobalStateStore.setState({ dateRange: DateRange.Yesterday });
},
@ -47,13 +49,13 @@ export const DateFilter: React.FC<DateFilterProps> = React.memo((props) => {
type: 'divider',
},
{
label: 'This week',
label: t('This week'),
onClick: () => {
useGlobalStateStore.setState({ dateRange: DateRange.ThisWeek });
},
},
{
label: 'Last 7 days',
label: t('Last 7 days'),
onClick: () => {
useGlobalStateStore.setState({ dateRange: DateRange.Last7Days });
},
@ -62,25 +64,25 @@ export const DateFilter: React.FC<DateFilterProps> = React.memo((props) => {
type: 'divider',
},
{
label: 'This Month',
label: t('This Month'),
onClick: () => {
useGlobalStateStore.setState({ dateRange: DateRange.ThisMonth });
},
},
{
label: 'Last 30 days',
label: t('Last 30 days'),
onClick: () => {
useGlobalStateStore.setState({ dateRange: DateRange.Last30Days });
},
},
{
label: 'Last 90 days',
label: t('Last 90 days'),
onClick: () => {
useGlobalStateStore.setState({ dateRange: DateRange.Last90Days });
},
},
{
label: 'This year',
label: t('This year'),
onClick: () => {
useGlobalStateStore.setState({ dateRange: DateRange.ThisYear });
},
@ -89,7 +91,7 @@ export const DateFilter: React.FC<DateFilterProps> = React.memo((props) => {
type: 'divider',
},
{
label: 'Custom',
label: t('Custom'),
onClick: () => {
setShowPicker(true);
},
@ -123,7 +125,7 @@ export const DateFilter: React.FC<DateFilterProps> = React.memo((props) => {
{showPicker && (
<Modal
title="Select your date range"
title={t('Select your date range')}
open={showPicker}
onCancel={() => setShowPicker(false)}
onOk={() => {

View File

@ -0,0 +1,73 @@
import React from 'react';
import { setLanguage, useTranslation } from '@i18next-toolkit/react';
import { Button, Dropdown } from 'antd';
import { LuLanguages } from 'react-icons/lu';
export const LanguageSelector: React.FC = React.memo(() => {
const { i18n } = useTranslation();
return (
<Dropdown
trigger={['click']}
placement="bottomRight"
menu={{
selectedKeys: [i18n.language],
items: [
{
label: 'English',
key: 'en',
itemIcon: <CountryFlag code="en" />,
},
{
label: 'Deutsch',
key: 'de',
itemIcon: <CountryFlag code="de" />,
},
{
label: 'Français',
key: 'fr',
itemIcon: <CountryFlag code="fr" />,
},
{
label: '日本語',
key: 'jp',
itemIcon: <CountryFlag code="jp" />,
},
{
label: 'Русский',
key: 'ru',
itemIcon: <CountryFlag code="ru" />,
},
{
label: '简体中文',
key: 'zh',
itemIcon: <CountryFlag code="zh" />,
},
],
onClick: (info) => {
setLanguage(info.key);
},
}}
>
<Button
icon={<LuLanguages className="anticon" />}
shape="circle"
size="large"
/>
</Dropdown>
);
});
LanguageSelector.displayName = 'LanguageSelector';
/**
* image is from discord
*/
export const CountryFlag: React.FC<{ code: string }> = React.memo((props) => {
return (
<img
className="w-[27px] h-[18px] ml-6"
src={`/locales/${props.code}/flag.png`}
/>
);
});
CountryFlag.displayName = 'CountryFlag';

View File

@ -1,6 +1,9 @@
import React from 'react';
import { useTranslation } from '@i18next-toolkit/react';
export const NoWorkspaceTip: React.FC = React.memo(() => {
return <div>Please Select Workspace</div>;
const { t } = useTranslation();
return <div>{t('Please Select Workspace')}</div>;
});
NoWorkspaceTip.displayName = 'NoWorkspaceTip';

View File

@ -5,8 +5,10 @@ import { useCurrentWorkspaceId } from '../../store/user';
import { useDashboardStore } from '../../store/dashboard';
import { DownOutlined } from '@ant-design/icons';
import clsx from 'clsx';
import { useTranslation } from '@i18next-toolkit/react';
export const DashboardItemAddButton: React.FC = React.memo(() => {
const { t } = useTranslation();
const workspaceId = useCurrentWorkspaceId();
const { data: websites = [], isLoading: isWebsiteLoading } =
trpc.website.all.useQuery({
@ -25,7 +27,7 @@ export const DashboardItemAddButton: React.FC = React.memo(() => {
items: [
{
key: 'website',
label: 'Website',
label: t('Website'),
children:
websites.length > 0
? websites.map((website) => ({
@ -34,7 +36,7 @@ export const DashboardItemAddButton: React.FC = React.memo(() => {
children: [
{
key: `website#${website.id}#overview`,
label: 'Overview',
label: t('Overview'),
onClick: () => {
addItem(
'websiteOverview',
@ -45,7 +47,7 @@ export const DashboardItemAddButton: React.FC = React.memo(() => {
},
{
key: `website#${website.id}#events`,
label: 'Events',
label: t('Events'),
onClick: () => {
addItem(
'websiteEvents',
@ -59,14 +61,14 @@ export const DashboardItemAddButton: React.FC = React.memo(() => {
: [
{
key: `website#none`,
label: '(None)',
label: t('(None)'),
disabled: true,
},
],
},
{
key: 'monitor',
label: 'Monitor',
label: t('Monitor'),
children:
monitors.length > 0
? monitors.map((monitor) => ({
@ -75,45 +77,53 @@ export const DashboardItemAddButton: React.FC = React.memo(() => {
children: [
{
key: `monitor#${monitor.id}#healthBar`,
label: 'Health Bar',
label: t('Health Bar'),
onClick: () => {
addItem(
'monitorHealthBar',
monitor.id,
`${monitor.name}'s Health`
t("{{monitorName}}'s Health", {
monitorName: monitor.name,
})
);
},
},
{
key: `monitor#${monitor.id}#metrics`,
label: 'Metrics',
label: t('Metrics'),
onClick: () => {
addItem(
'monitorMetrics',
monitor.id,
`${monitor.name}'s Metrics`
t("{{monitorName}}'s Metrics", {
monitorName: monitor.name,
})
);
},
},
{
key: `monitor#${monitor.id}#chart`,
label: 'Chart',
label: t('Chart'),
onClick: () => {
addItem(
'monitorChart',
monitor.id,
`${monitor.name}'s Chart`
t("{{monitorName}}'s Chart", {
monitorName: monitor.name,
})
);
},
},
{
key: `monitor#${monitor.id}#events`,
label: 'Events',
label: t('Events'),
onClick: () => {
addItem(
'monitorEvents',
monitor.id,
`${monitor.name}'s Events`
t("{{monitorName}}'s Events", {
monitorName: monitor.name,
})
);
},
},
@ -122,7 +132,7 @@ export const DashboardItemAddButton: React.FC = React.memo(() => {
: [
{
key: `monitor#none`,
label: '(None)',
label: t('(None)'),
disabled: true,
},
],
@ -141,7 +151,7 @@ export const DashboardItemAddButton: React.FC = React.memo(() => {
>
<Button type="primary" size="large" className="w-32">
<Space>
<span>Add</span>
<span>{t('Add')}</span>
<DownOutlined
className={clsx(
'transition-transform scale-y-75',

View File

@ -9,8 +9,10 @@ import { DateFilter } from '../DateFilter';
import { trpc } from '../../api/trpc';
import { useCurrentWorkspace, useCurrentWorkspaceId } from '../../store/user';
import clsx from 'clsx';
import { useTranslation } from '@i18next-toolkit/react';
export const Dashboard: React.FC = React.memo(() => {
const { t } = useTranslation();
const { isEditMode, switchEditMode, layouts, items } = useDashboardStore();
const mutation = trpc.workspace.saveDashboardLayout.useMutation();
const workspaceId = useCurrentWorkspaceId();
@ -42,7 +44,7 @@ export const Dashboard: React.FC = React.memo(() => {
},
});
switchEditMode();
message.success('Layout saved success');
message.success(t('Layout saved success'));
});
return (
@ -63,7 +65,7 @@ export const Dashboard: React.FC = React.memo(() => {
disabled={mutation.isLoading}
onClick={handleSaveDashboardLayout}
>
Done
{t('Done')}
</Button>
</>
) : (
@ -75,7 +77,7 @@ export const Dashboard: React.FC = React.memo(() => {
size="large"
onClick={switchEditMode}
>
Edit
{t('Edit')}
</Button>
</>
)}
@ -88,7 +90,11 @@ export const Dashboard: React.FC = React.memo(() => {
/>
{items.length === 0 && (
<Empty description="You have not dashboard item yet, please enter edit mode and add you item." />
<Empty
description={t(
'You have not dashboard item yet, please enter edit mode and add you item.'
)}
/>
)}
</div>
);

View File

@ -7,10 +7,12 @@ import { WebsiteOverview } from '../../website/WebsiteOverview';
import { Button } from 'antd';
import { useNavigate } from 'react-router';
import { ArrowRightOutlined } from '@ant-design/icons';
import { useTranslation } from '@i18next-toolkit/react';
export const WebsiteOverviewItem: React.FC<{
websiteId: string;
}> = React.memo((props) => {
const { t } = useTranslation();
const workspaceId = useCurrentWorkspaceId();
const navigate = useNavigate();
@ -39,7 +41,7 @@ export const WebsiteOverviewItem: React.FC<{
navigate(`/website/${websiteInfo.id}`);
}}
>
View Details <ArrowRightOutlined />
{t('View Details')} <ArrowRightOutlined />
</Button>
</>
}

View File

@ -16,6 +16,7 @@ import {
import { useEvent } from '../../../hooks/useEvent';
import { useCurrentWorkspaceId } from '../../../store/user';
import { notificationStrategies } from './strategies';
import { useTranslation } from '@i18next-toolkit/react';
export interface NotificationFormValues {
id?: string;
@ -36,6 +37,7 @@ interface NotificationInfoModalProps
}
export const NotificationInfoModal: React.FC<NotificationInfoModalProps> =
React.memo((props) => {
const { t } = useTranslation();
const [form] = Form.useForm();
const typeValue = Form.useWatch('type', form);
const currentWorkspaceId = useCurrentWorkspaceId()!;
@ -85,7 +87,7 @@ export const NotificationInfoModal: React.FC<NotificationInfoModalProps> =
return (
<Modal
title="Notification"
title={t('Notification')}
destroyOnClose={true}
maskClosable={false}
centered={true}
@ -94,10 +96,10 @@ export const NotificationInfoModal: React.FC<NotificationInfoModalProps> =
footer={
<div className="space-x-2">
<Button loading={testMutation.isLoading} onClick={handleTest}>
Test
{t('Test')}
</Button>
<Button type="primary" onClick={handleSave}>
Save
{t('Save')}
</Button>
</div>
}
@ -110,7 +112,7 @@ export const NotificationInfoModal: React.FC<NotificationInfoModalProps> =
initialValues={props.initialValues ?? defaultValues}
>
<Form.Item hidden name="id" />
<Form.Item label="Notification Type" name="type">
<Form.Item label={t('Notification Type')} name="type">
<Select>
{notificationStrategies.map((s) => (
<Select.Option key={s.name} value={s.name}>
@ -120,7 +122,7 @@ export const NotificationInfoModal: React.FC<NotificationInfoModalProps> =
</Select>
</Form.Item>
<Form.Item label="Display Name" name="name">
<Form.Item label={t('Display Name')} name="name">
<Input />
</Form.Item>

View File

@ -1,18 +1,21 @@
import { Form, Input } from 'antd';
import React from 'react';
import { useTranslation } from '@i18next-toolkit/react';
export const NotificationApprise: React.FC = React.memo(() => {
const { t } = useTranslation();
return (
<>
<Form.Item
label="Apprise URL"
label={t('Apprise URL')}
name={['payload', 'appriseUrl']}
rules={[{ required: true }]}
>
<Input placeholder="For example: pushdeer://pushKey" />
<Input placeholder={t('For example: pushdeer://pushKey')} />
</Form.Item>
<div className="text-sm opacity-80">
Read more:{' '}
{t('Read more')}:{' '}
<a
href="https://github.com/caronc/apprise/wiki#notification-services"
target="_blank"

View File

@ -1,68 +1,73 @@
import { Checkbox, Form, Input, InputNumber, Select } from 'antd';
import React from 'react';
import { useTranslation } from '@i18next-toolkit/react';
export const NotificationSMTP: React.FC = React.memo(() => {
const { t } = useTranslation();
return (
<>
<Form.Item
label="Host"
label={t('Host')}
name={['payload', 'hostname']}
rules={[{ required: true }]}
>
<Input />
</Form.Item>
<Form.Item
label="Port"
label={t('Port')}
name={['payload', 'port']}
rules={[{ required: true }, { type: 'number', min: 0, max: 65535 }]}
>
<InputNumber max={65535} min={1} />
</Form.Item>
<Form.Item
label="Security"
label={t('Security')}
name={['payload', 'security']}
initialValue={false}
>
<Select>
<Select.Option value={false}>None / STARTTLS (25, 587)</Select.Option>
<Select.Option value={false}>
{t('None / STARTTLS')} (25, 587)
</Select.Option>
<Select.Option value={true}>TLS (465)</Select.Option>
</Select>
</Form.Item>
<Form.Item name={['payload', 'ignoreTLS']} valuePropName="checked">
<Checkbox>Ignore TLS Error</Checkbox>
<Checkbox>{t('Ignore TLS Error')}</Checkbox>
</Form.Item>
<Form.Item
label="Username"
label={t('Username')}
name={['payload', 'username']}
rules={[{ required: true }]}
>
<Input />
</Form.Item>
<Form.Item
label="Password"
label={t('Password')}
name={['payload', 'password']}
rules={[{ required: true }]}
>
<Input.Password />
</Form.Item>
<Form.Item
label="From Email"
label={t('From Email')}
name={['payload', 'from']}
rules={[{ required: true }]}
>
<Input />
</Form.Item>
<Form.Item
label="To Email"
label={t('To Email')}
name={['payload', 'to']}
rules={[{ required: true }]}
>
<Input />
</Form.Item>
<Form.Item label="CC" name={['payload', 'cc']}>
<Form.Item label={t('CC')} name={['payload', 'cc']}>
<Input />
</Form.Item>
<Form.Item label="BCC" name={['payload', 'bcc']}>
<Form.Item label={t('BCC')} name={['payload', 'bcc']}>
<Input />
</Form.Item>
</>

View File

@ -4,8 +4,10 @@ import { useEvent } from '../../../../hooks/useEvent';
import axios from 'axios';
import { AutoLoadingButton } from '../../../AutoLoadingButton';
import { get, last } from 'lodash-es';
import { useTranslation } from '@i18next-toolkit/react';
export const NotificationTelegram: React.FC = React.memo(() => {
const { t } = useTranslation();
const token = Form.useWatch(['payload', 'botToken']);
const form = Form.useFormInstance();
@ -32,16 +34,16 @@ export const NotificationTelegram: React.FC = React.memo(() => {
return (
<>
<Form.Item
label="Bot Token"
label={t('Bot Token')}
name={['payload', 'botToken']}
rules={[{ required: true }]}
>
<Input.Password />
</Form.Item>
<Typography.Paragraph className="text-neutral-500">
You can get a token from https://t.me/BotFather.
{t('You can get a token from https://t.me/BotFather.')}
</Typography.Paragraph>
<Form.Item label="Chat ID" required={true}>
<Form.Item label={t('Chat ID')} required={true}>
<div className="flex gap-2 overflow-hidden">
<Form.Item
className="flex-1"
@ -54,17 +56,19 @@ export const NotificationTelegram: React.FC = React.memo(() => {
{token && (
<AutoLoadingButton onClick={handleAutoGet}>
Auto Fetch
{t('Auto Fetch')}
</AutoLoadingButton>
)}
</div>
</Form.Item>
<Typography.Paragraph className="text-neutral-500">
Support Direct Chat / Group / Channel's Chat ID
{t("Support Direct Chat / Group / Channel's Chat ID")}
</Typography.Paragraph>
<Typography.Paragraph className="text-neutral-500">
You can get your chat ID by sending a message to the bot and going to
this URL to view the chat_id:
{t(
'You can get your chat ID by sending a message to the bot and going to this URL to view the chat_id'
)}
:
</Typography.Paragraph>
<Typography.Link href={getUpdatesUrl(token)} target="_blank">
{getUpdatesUrl('*'.repeat(token?.length ?? 0))}

View File

@ -2,12 +2,14 @@ import { Checkbox, Divider, Input, message } from 'antd';
import React, { useMemo, useState } from 'react';
import { useEvent } from '../../hooks/useEvent';
import copy from 'copy-to-clipboard';
import { useTranslation } from '@i18next-toolkit/react';
export const MonitorBadgeView: React.FC<{
workspaceId: string;
monitorId: string;
monitorName?: string;
}> = React.memo((props) => {
const { t } = useTranslation();
const { workspaceId, monitorId, monitorName = '' } = props;
const [showDetail, setShowDetail] = useState(false);
@ -24,7 +26,7 @@ export const MonitorBadgeView: React.FC<{
const handleCopy = useEvent((text: string) => {
copy(text);
message.success('Copy success!');
message.success(t('Copy success!'));
});
return (
@ -32,19 +34,19 @@ export const MonitorBadgeView: React.FC<{
<div>
<img src={url} />
</div>
<p>This will show your recent result of your monitor</p>
<p>{t('This will show your recent result of your monitor')}</p>
<div>
<Checkbox
checked={showDetail}
onChange={(e) => setShowDetail(e.target.checked)}
>
Show Detail Number
{t('Show Detail Number')}
</Checkbox>
</div>
<Divider />
<p>Share with...</p>
<p>{t('Share with...')}</p>
<div className="flex flex-col gap-2">
<Input
@ -57,7 +59,7 @@ export const MonitorBadgeView: React.FC<{
handleCopy(`<img src="${url}" title="${monitorName}" />`)
}
>
Copy
{t('Copy')}
</div>
}
/>
@ -70,7 +72,7 @@ export const MonitorBadgeView: React.FC<{
className="cursor-pointer"
onClick={() => handleCopy(`![${monitorName}](${url})`)}
>
Copy
{t('Copy')}
</div>
}
/>
@ -83,7 +85,7 @@ export const MonitorBadgeView: React.FC<{
className="cursor-pointer"
onClick={() => handleCopy(`[img]${url}[/img]`)}
>
Copy
{t('Copy')}
</div>
}
/>
@ -93,7 +95,7 @@ export const MonitorBadgeView: React.FC<{
value={url}
addonAfter={
<div className="cursor-pointer" onClick={() => handleCopy(url)}>
Copy
{t('Copy')}
</div>
}
/>

View File

@ -7,9 +7,11 @@ import { useSocketSubscribeList } from '../../api/socketio';
import { trpc } from '../../api/trpc';
import { useCurrentWorkspaceId } from '../../store/user';
import { getMonitorProvider, getProviderDisplay } from './provider';
import { useTranslation } from '@i18next-toolkit/react';
export const MonitorDataChart: React.FC<{ monitorId: string }> = React.memo(
(props) => {
const { t } = useTranslation();
const workspaceId = useCurrentWorkspaceId();
const { monitorId } = props;
const [rangeType, setRangeType] = useState('recent');
@ -145,11 +147,11 @@ export const MonitorDataChart: React.FC<{ monitorId: string }> = React.memo(
value={rangeType}
onChange={(val) => setRangeType(val)}
>
<Select.Option value="recent">Recent</Select.Option>
<Select.Option value="3h">3h</Select.Option>
<Select.Option value="6h">6h</Select.Option>
<Select.Option value="24h">24h</Select.Option>
<Select.Option value="1w">1w</Select.Option>
<Select.Option value="recent">{t('Recent')}</Select.Option>
<Select.Option value="3h">{t('3h')}</Select.Option>
<Select.Option value="6h">{t('6h')}</Select.Option>
<Select.Option value="24h">{t('24h')}</Select.Option>
<Select.Option value="1w">{t('1w')}</Select.Option>
</Select>
</div>

View File

@ -5,12 +5,14 @@ import { ErrorTip } from '../ErrorTip';
import { Loading } from '../Loading';
import { getMonitorProvider } from './provider';
import { MonitorStatsBlock } from './MonitorStatsBlock';
import { useTranslation } from '@i18next-toolkit/react';
export const MonitorDataMetrics: React.FC<{
monitorId: string;
monitorType: string;
currectResponse?: number;
}> = React.memo((props) => {
const { t } = useTranslation();
const workspaceId = useCurrentWorkspaceId();
const { monitorId, monitorType, currectResponse } = props;
const { data, isLoading } = trpc.monitor.dataMetrics.useQuery({
@ -53,20 +55,20 @@ export const MonitorDataMetrics: React.FC<{
<div className="flex justify-between text-center">
{typeof currectResponse === 'number' && (
<MonitorStatsBlock
title="Response"
desc="(Current)"
title={t('Response')}
desc={t('(Current)')}
text={formatterFn(currectResponse)}
/>
)}
<MonitorStatsBlock
title="Avg. Response"
desc="(24 hour)"
title={t('Avg. Response')}
desc={t('(24 hour)')}
text={formatterFn(parseFloat(data.recent1DayAvg.toFixed(0)))}
/>
<MonitorStatsBlock
title="Uptime"
desc="(24 hour)"
title={t('Uptime')}
desc={t('(24 hour)')}
text={`${parseFloat(
(
(data.recent1DayOnlineCount /
@ -76,8 +78,8 @@ export const MonitorDataMetrics: React.FC<{
)} %`}
/>
<MonitorStatsBlock
title="Uptime"
desc="(30 days)"
title={t('Uptime')}
desc={t('(30 days)')}
text={`${parseFloat(
(
(data.recent30DayOnlineCount /

View File

@ -5,12 +5,14 @@ import clsx from 'clsx';
import dayjs from 'dayjs';
import { Card, Empty } from 'antd';
import { useNavigate } from 'react-router';
import { useTranslation } from '@i18next-toolkit/react';
interface MonitorEventListProps {
monitorId?: string;
}
export const MonitorEventList: React.FC<MonitorEventListProps> = React.memo(
(props) => {
const { t } = useTranslation();
const workspaceId = useCurrentWorkspaceId();
const { data = [], isLoading } = trpc.monitor.events.useQuery({
workspaceId,
@ -19,7 +21,7 @@ export const MonitorEventList: React.FC<MonitorEventListProps> = React.memo(
const navigate = useNavigate();
if (isLoading === false && data.length === 0) {
return <Empty description="No events" />;
return <Empty description={t('No events')} />;
}
return (

View File

@ -7,6 +7,7 @@ import { trpc } from '../../api/trpc';
import { useWatch } from '../../hooks/useWatch';
import { getMonitorProvider, getProviderDisplay } from './provider';
import { MonitorProvider } from './provider/types';
import { useTranslation } from '@i18next-toolkit/react';
interface MonitorHealthBarProps {
workspaceId: string;
@ -21,6 +22,7 @@ interface MonitorHealthBarProps {
}
export const MonitorHealthBar: React.FC<MonitorHealthBarProps> = React.memo(
(props) => {
const { t } = useTranslation();
const {
workspaceId,
monitorId,
@ -92,15 +94,15 @@ export const MonitorHealthBar: React.FC<MonitorHealthBarProps> = React.memo(
<>
{last(beats)?.status === 'health' ? (
<div className="bg-green-500 text-white px-4 py-1 rounded-full text-lg font-bold">
UP
{t('UP')}
</div>
) : last(beats)?.status === 'error' ? (
<div className="bg-red-600 text-white px-4 py-1 rounded-full text-lg font-bold">
DOWN
{t('DOWN')}
</div>
) : (
<div className="bg-gray-400 text-white px-4 py-1 rounded-full text-lg font-bold">
NONE
{t('NONE')}
</div>
)}
</>

View File

@ -21,11 +21,13 @@ import { MonitorDataMetrics } from './MonitorDataMetrics';
import { MonitorDataChart } from './MonitorDataChart';
import { DeleteOutlined, MoreOutlined } from '@ant-design/icons';
import { MonitorBadgeView } from './MonitorBadgeView';
import { useTranslation } from '@i18next-toolkit/react';
interface MonitorInfoProps {
monitorId: string;
}
export const MonitorInfo: React.FC<MonitorInfoProps> = React.memo((props) => {
const { t } = useTranslation();
const workspaceId = useCurrentWorkspaceId();
const { monitorId } = props;
const [currectResponse, setCurrentResponse] = useState(0);
@ -95,8 +97,8 @@ export const MonitorInfo: React.FC<MonitorInfoProps> = React.memo((props) => {
const handleDelete = useEvent(async () => {
Modal.confirm({
title: 'Warning',
content: 'Did you sure delete this monitor?',
title: t('Warning'),
content: t('Did you sure delete this monitor?'),
okButtonProps: {
danger: true,
},
@ -116,8 +118,8 @@ export const MonitorInfo: React.FC<MonitorInfoProps> = React.memo((props) => {
const handleClearEvents = useEvent(() => {
Modal.confirm({
title: 'Warning',
content: 'Are you sure want to delete all events for this monitor?',
title: t('Warning'),
content: t('Are you sure want to delete all events for this monitor?'),
okButtonProps: {
danger: true,
},
@ -136,8 +138,10 @@ export const MonitorInfo: React.FC<MonitorInfoProps> = React.memo((props) => {
const handleClearData = useEvent(() => {
Modal.confirm({
title: 'Warning',
content: 'Are you sure want to delete all heartbeats for this monitor?',
title: t('Warning'),
content: t(
'Are you sure want to delete all heartbeats for this monitor?'
),
okButtonProps: {
danger: true,
},
@ -170,7 +174,7 @@ export const MonitorInfo: React.FC<MonitorInfoProps> = React.memo((props) => {
<span>{monitorInfo.name}</span>
{monitorInfo.active === false && (
<div className="bg-red-500 rounded-full px-2 py-0.5 text-white text-xs">
Stopped
{t('Stopped')}
</div>
)}
</div>
@ -184,8 +188,9 @@ export const MonitorInfo: React.FC<MonitorInfoProps> = React.memo((props) => {
</Space>
<div className="text-black dark:text-gray-200 text-opacity-75">
Monitored for {dayjs().diff(dayjs(monitorInfo.createdAt), 'days')}{' '}
days
{t('Monitored for {{dayNum}} days', {
dayNum: dayjs().diff(dayjs(monitorInfo.createdAt), 'days'),
})}
</div>
</div>
@ -196,7 +201,7 @@ export const MonitorInfo: React.FC<MonitorInfoProps> = React.memo((props) => {
navigate(`/monitor/${monitorInfo.id}/edit`);
}}
>
Edit
{t('Edit')}
</Button>
{monitorInfo.active ? (
@ -204,14 +209,14 @@ export const MonitorInfo: React.FC<MonitorInfoProps> = React.memo((props) => {
loading={changeActiveMutation.isLoading}
onClick={handleStop}
>
Stop
{t('Stop')}
</Button>
) : (
<Button
loading={changeActiveMutation.isLoading}
onClick={handleStart}
>
Start
{t('Start')}
</Button>
)}
@ -222,7 +227,7 @@ export const MonitorInfo: React.FC<MonitorInfoProps> = React.memo((props) => {
items: [
{
key: 'badge',
label: 'Show Badge',
label: t('Show Badge'),
onClick: () => setShowBadge(true),
},
{
@ -230,7 +235,7 @@ export const MonitorInfo: React.FC<MonitorInfoProps> = React.memo((props) => {
},
{
key: 'delete',
label: 'Delete',
label: t('Delete'),
danger: true,
onClick: handleDelete,
},
@ -288,19 +293,19 @@ export const MonitorInfo: React.FC<MonitorInfoProps> = React.memo((props) => {
items: [
{
key: 'events',
label: 'Events',
label: t('Events'),
onClick: handleClearEvents,
},
{
key: 'heartbeats',
label: 'Heartbeats',
label: t('Heartbeats'),
onClick: handleClearData,
},
],
}}
>
<Button icon={<DeleteOutlined />} danger={true}>
Clear Data
{t('Clear Data')}
</Button>
</Dropdown>
</div>

View File

@ -4,6 +4,7 @@ import { Button, Form, Input, InputNumber, Select } from 'antd';
import { getMonitorProvider, monitorProviders } from './provider';
import { useEventWithLoading } from '../../hooks/useEvent';
import { NotificationPicker } from '../notification/NotificationPicker';
import { useTranslation } from '@i18next-toolkit/react';
export type MonitorInfoEditorValues = Omit<
Monitor,
@ -28,6 +29,7 @@ interface MonitorInfoEditorProps {
}
export const MonitorInfoEditor: React.FC<MonitorInfoEditorProps> = React.memo(
(props) => {
const { t } = useTranslation();
const [form] = Form.useForm();
const typeValue = Form.useWatch('type', form);
const initialValues = props.initialValues ?? defaultValues;
@ -65,7 +67,7 @@ export const MonitorInfoEditor: React.FC<MonitorInfoEditorProps> = React.memo(
>
<Form.Item hidden name="id" />
<Form.Item label="Monitor Type" name="type">
<Form.Item label={t('Monitor Type')} name="type">
<Select disabled={isEdit}>
{monitorProviders.map((m) => (
<Select.Option key={m.name} value={m.name}>
@ -75,12 +77,12 @@ export const MonitorInfoEditor: React.FC<MonitorInfoEditorProps> = React.memo(
</Select>
</Form.Item>
<Form.Item label="Name" name="name" rules={[{ required: true }]}>
<Form.Item label={t('Name')} name="name" rules={[{ required: true }]}>
<Input />
</Form.Item>
<Form.Item
label="Check Interval(s)"
label={t('Check Interval(s)')}
name="interval"
rules={[{ required: true }]}
>
@ -92,21 +94,23 @@ export const MonitorInfoEditor: React.FC<MonitorInfoEditorProps> = React.memo(
</Form.Item>
<Form.Item
label="Max Retries"
label={t('Max Retries')}
name="maxRetries"
tooltip="Maximum retries before the service is marked as down and a notification is sent"
tooltip={t(
'Maximum retries before the service is marked as down and a notification is sent'
)}
>
<InputNumber min={0} max={10} defaultValue={0} />
</Form.Item>
{formEl}
<Form.Item label="Notification" name="notificationIds">
<Form.Item label={t('Notification')} name="notificationIds">
<NotificationPicker allowClear={true} mode="multiple" />
</Form.Item>
<Button type="primary" htmlType="submit" loading={isLoading}>
Save
{t('Save')}
</Button>
</Form>
</div>

View File

@ -7,8 +7,10 @@ import { Empty } from 'antd';
import { MonitorListItem } from './MonitorListItem';
import { useNavigate, useParams } from 'react-router';
import clsx from 'clsx';
import { useTranslation } from '@i18next-toolkit/react';
export const MonitorList: React.FC = React.memo(() => {
const { t } = useTranslation();
const workspaceId = useCurrentWorkspaceId();
const { data: monitors = [], isLoading } = trpc.monitor.all.useQuery({
workspaceId,
@ -32,7 +34,9 @@ export const MonitorList: React.FC = React.memo(() => {
return (
<div className="p-2">
{monitors.length === 0 && <Empty description="Here is no monitor yet." />}
{monitors.length === 0 && (
<Empty description={t('Here is no monitor yet.')} />
)}
{monitors.map((monitor) => (
<MonitorListItem

View File

@ -4,6 +4,7 @@ import { MonitorHealthBar } from './MonitorHealthBar';
import { last } from 'lodash-es';
import { getMonitorProvider, getProviderDisplay } from './provider';
import { Tooltip } from 'antd';
import { useTranslation } from '@i18next-toolkit/react';
export const MonitorListItem: React.FC<{
className?: string;
@ -14,6 +15,7 @@ export const MonitorListItem: React.FC<{
showCurrentResponse?: boolean;
onClick?: () => void;
}> = React.memo((props) => {
const { t } = useTranslation();
const {
className,
workspaceId,
@ -98,7 +100,7 @@ export const MonitorListItem: React.FC<{
</div>
{showCurrentResponse && latestResponse && (
<Tooltip title="Current">
<Tooltip title={t('Current')}>
<div className="px-2 text-sm text-gray-800 dark:text-gray-400">
{latestResponse}
</div>

View File

@ -3,17 +3,19 @@ import React from 'react';
import { trpc } from '../../api/trpc';
import { useCurrentWorkspaceId } from '../../store/user';
import { ColorTag } from '../ColorTag';
import { useTranslation } from '@i18next-toolkit/react';
interface MonitorPickerProps extends SelectProps<string> {}
export const MonitorPicker: React.FC<MonitorPickerProps> = React.memo(
(props) => {
const { t } = useTranslation();
const workspaceId = useCurrentWorkspaceId();
const { data: allMonitor = [] } = trpc.monitor.all.useQuery({
workspaceId,
});
return (
<Select placeholder="Select monitor" {...props}>
<Select placeholder={t('Select monitor')} {...props}>
{allMonitor.map((m) => (
<Select.Option key={m.id} value={m.id}>
<ColorTag label={m.type} />

View File

@ -3,6 +3,7 @@ import React from 'react';
import { MinusCircleOutlined, PlusOutlined } from '@ant-design/icons';
import { MonitorPicker } from '../MonitorPicker';
import { urlSlugValidator } from '../../../utils/validator';
import { useTranslation } from '@i18next-toolkit/react';
const { Text } = Typography;
@ -22,6 +23,8 @@ interface MonitorStatusPageEditFormProps {
export const MonitorStatusPageEditForm: React.FC<MonitorStatusPageEditFormProps> =
React.memo((props) => {
const { t } = useTranslation();
return (
<div>
<Form<MonitorStatusPageEditFormValues>
@ -30,7 +33,7 @@ export const MonitorStatusPageEditForm: React.FC<MonitorStatusPageEditFormProps>
onFinish={props.onFinish}
>
<Form.Item
label="Title"
label={t('Title')}
name="title"
rules={[
{
@ -47,11 +50,11 @@ export const MonitorStatusPageEditForm: React.FC<MonitorStatusPageEditFormProps>
extra={
<div className="pt-2">
<div>
Accept characters: <Text code>a-z</Text> <Text code>0-9</Text>{' '}
<Text code>-</Text>
{t('Accept characters')}: <Text code>a-z</Text>{' '}
<Text code>0-9</Text> <Text code>-</Text>
</div>
<div>
No consecutive dashes <Text code>--</Text>
{t('No consecutive dashes')} <Text code>--</Text>
</div>
</div>
}
@ -71,7 +74,7 @@ export const MonitorStatusPageEditForm: React.FC<MonitorStatusPageEditFormProps>
{(fields, { add, remove }, { errors }) => {
return (
<>
<Form.Item label="Monitors">
<Form.Item label={t('Monitors')}>
<div className="flex flex-col gap-2 mb-2">
{fields.map((field, index) => (
// monitor item
@ -84,7 +87,7 @@ export const MonitorStatusPageEditForm: React.FC<MonitorStatusPageEditFormProps>
rules={[
{
required: true,
message: 'Please select monitor',
message: t('Please select monitor'),
},
]}
noStyle={true}
@ -103,7 +106,7 @@ export const MonitorStatusPageEditForm: React.FC<MonitorStatusPageEditFormProps>
</Form.Item>
<span className="text-sm align-middle ml-1">
Show Current Response
{t('Show Current Response')}
</span>
</div>
@ -123,7 +126,7 @@ export const MonitorStatusPageEditForm: React.FC<MonitorStatusPageEditFormProps>
style={{ width: '60%' }}
icon={<PlusOutlined />}
>
Add Monitor
{t('Add Monitor')}
</Button>
<Form.ErrorList errors={errors} />
@ -135,12 +138,12 @@ export const MonitorStatusPageEditForm: React.FC<MonitorStatusPageEditFormProps>
<div className="flex gap-4">
<Button type="primary" htmlType="submit" loading={props.isLoading}>
{props.saveButtonLabel ?? 'Save'}
{props.saveButtonLabel ?? t('Save')}
</Button>
{props.onCancel && (
<Button htmlType="button" onClick={props.onCancel}>
Cancel
{t('Cancel')}
</Button>
)}
</div>

View File

@ -4,6 +4,7 @@ import { trpc } from '../../../api/trpc';
import { Loading } from '../../Loading';
import { MonitorListItem } from '../MonitorListItem';
import { keyBy } from 'lodash-es';
import { useTranslation } from '@i18next-toolkit/react';
interface StatusPageServicesProps {
workspaceId: string;
@ -14,6 +15,7 @@ interface StatusPageServicesProps {
}
export const StatusPageServices: React.FC<StatusPageServicesProps> = React.memo(
(props) => {
const { t } = useTranslation();
const { workspaceId, monitorList } = props;
const { data: list = [], isLoading } = trpc.monitor.getPublicInfo.useQuery({
@ -40,7 +42,7 @@ export const StatusPageServices: React.FC<StatusPageServicesProps> = React.memo(
/>
))
) : (
<Empty description="No any monitor has been set" />
<Empty description={t('No any monitor has been set')} />
)}
</div>
);

View File

@ -11,6 +11,7 @@ import { useRequest } from '../../../hooks/useRequest';
import { useNavigate } from 'react-router';
import { ColorSchemeSwitcher } from '../../ColorSchemeSwitcher';
import { StatusPageServices } from './Services';
import { useTranslation } from '@i18next-toolkit/react';
interface MonitorStatusPageProps {
slug: string;
@ -18,6 +19,7 @@ interface MonitorStatusPageProps {
export const MonitorStatusPage: React.FC<MonitorStatusPageProps> = React.memo(
(props) => {
const { t } = useTranslation();
const { slug } = props;
const { data: info } = trpc.monitor.getPageInfo.useQuery({
@ -88,16 +90,16 @@ export const MonitorStatusPage: React.FC<MonitorStatusPageProps> = React.memo(
{allowEdit && !editMode && (
<div className="mb-4 flex gap-2">
<Button type="primary" onClick={() => setEditMode(true)}>
Edit
{t('Edit')}
</Button>
<Button type="default" onClick={() => navigate(`/`)}>
Back to Admin
{t('Back to Admin')}
</Button>
</div>
)}
<div className="text-lg mb-2">Services</div>
<div className="text-lg mb-2">{t('Services')}</div>
{info && (
<StatusPageServices

View File

@ -8,8 +8,10 @@ import { useCurrentWorkspaceId } from '../../../store/user';
import { useEvent } from '../../../hooks/useEvent';
import dayjs from 'dayjs';
import { ColorTag } from '../../ColorTag';
import { Trans, useTranslation } from '@i18next-toolkit/react';
export const MonitorCustom: React.FC = React.memo(() => {
const { t } = useTranslation();
const workspaceId = useCurrentWorkspaceId();
const testScriptMutation = trpc.monitor.testCustomScript.useMutation({
onError: defaultErrorHandler,
@ -26,7 +28,7 @@ export const MonitorCustom: React.FC = React.memo(() => {
modal.info({
centered: true,
maskClosable: true,
title: 'Run Completed',
title: t('Run Completed'),
width: 'clamp(320px, 60vw, 860px)',
content: (
<div>
@ -48,9 +50,9 @@ export const MonitorCustom: React.FC = React.memo(() => {
</div>
))}
<div>Usage: {usage}ms</div>
<div>{t('Usage: {{usage}}ms', { usage })} </div>
<div>
Result: <span className="font-semibold">{result}</span>
{t('Result')}: <span className="font-semibold">{result}</span>
</div>
</div>
),
@ -60,7 +62,7 @@ export const MonitorCustom: React.FC = React.memo(() => {
return (
<>
<Form.Item
label="Script JS Code"
label={t('Script JS Code')}
name={['payload', 'code']}
rules={[{ required: true }]}
>
@ -71,7 +73,7 @@ export const MonitorCustom: React.FC = React.memo(() => {
icon={<PlayCircleOutlined />}
onClick={handleTestCode}
>
Test Code
{t('Test Code')}
</Button>
{contextHolder}
</>
@ -80,7 +82,7 @@ export const MonitorCustom: React.FC = React.memo(() => {
MonitorCustom.displayName = 'MonitorCustom';
export const customProvider: MonitorProvider = {
label: 'Custom',
label: <Trans>Custom</Trans>,
name: 'custom',
form: MonitorCustom,
valueLabel: 'Result',

View File

@ -7,12 +7,15 @@ import dayjs from 'dayjs';
import { isEmpty } from 'lodash-es';
import { useCurrentWorkspaceId } from '../../../store/user';
import { z } from 'zod';
import { useTranslation } from '@i18next-toolkit/react';
const MonitorHttp: React.FC = React.memo(() => {
const { t } = useTranslation();
return (
<>
<Form.Item
label="Url"
label={t('Url')}
name={['payload', 'url']}
rules={[
{ required: true },
@ -45,11 +48,11 @@ const MonitorHttp: React.FC = React.memo(() => {
<Select.Option value="options">OPTIONS</Select.Option>
</Select>
</Form.Item>
<Form.Item label="Request Timeout(s)" name={['payload', 'timeout']}>
<Form.Item label={t('Request Timeout(s)')} name={['payload', 'timeout']}>
<InputNumber defaultValue={30} />
</Form.Item>
<Form.Item
label="Ignore TLS/SSL error"
label={t('Ignore TLS/SSL error')}
valuePropName="checked"
name={['payload', 'ignoreTLS']}
>
@ -137,6 +140,7 @@ MonitorHttp.displayName = 'MonitorHttp';
export const MonitorHttpOverview: MonitorOverviewComponent = React.memo(
(props) => {
const { t } = useTranslation();
const workspaceId = useCurrentWorkspaceId();
const { data } = trpc.monitor.getStatus.useQuery({
workspaceId,
@ -156,9 +160,11 @@ export const MonitorHttpOverview: MonitorOverviewComponent = React.memo(
return (
<MonitorStatsBlock
title="Cert Exp."
title={t('Cert Exp.')}
desc={dayjs(payload.certInfo?.validTo).format('YYYY-MM-DD')}
text={`${payload.certInfo?.daysRemaining} days`}
text={t('{{num}} days', {
num: payload.certInfo?.daysRemaining,
})}
/>
);
}

View File

@ -5,12 +5,15 @@ import { useCurrentWorkspaceId } from '../../../store/user';
import { trpc } from '../../../api/trpc';
import dayjs from 'dayjs';
import { MonitorStatsBlock } from '../MonitorStatsBlock';
import { useTranslation } from '@i18next-toolkit/react';
export const MonitorOpenai: React.FC = React.memo(() => {
const { t } = useTranslation();
return (
<>
<Form.Item
label="Session Key"
label={t('Session Key')}
name={['payload', 'sessionKey']}
rules={[{ required: true }]}
>
@ -26,6 +29,7 @@ MonitorOpenai.displayName = 'MonitorOpenai';
export const MonitorOpenaiOverview: MonitorOverviewComponent = React.memo(
(props) => {
const { t } = useTranslation();
const workspaceId = useCurrentWorkspaceId();
const { data } = trpc.monitor.getStatus.useQuery({
workspaceId,
@ -41,7 +45,7 @@ export const MonitorOpenaiOverview: MonitorOverviewComponent = React.memo(
return (
<MonitorStatsBlock
title="Usage"
title={t('Usage')}
desc={dayjs(data.updatedAt).format('YYYY-MM-DD')}
text={`$${payload.totalUsed} / $${payload.allUSD}`}
/>

View File

@ -2,12 +2,15 @@ import { Form, Input } from 'antd';
import React from 'react';
import { MonitorProvider } from './types';
import { hostnameValidator } from '../../../utils/validator';
import { useTranslation } from '@i18next-toolkit/react';
export const MonitorPing: React.FC = React.memo(() => {
const { t } = useTranslation();
return (
<>
<Form.Item
label="Host"
label={t('Host')}
name={['payload', 'hostname']}
rules={[
{ required: true },

View File

@ -2,12 +2,15 @@ import { Form, Input, InputNumber } from 'antd';
import React from 'react';
import { MonitorProvider } from './types';
import { hostnameValidator, portValidator } from '../../../utils/validator';
import { useTranslation } from '@i18next-toolkit/react';
export const MonitorTCP: React.FC = React.memo(() => {
const { t } = useTranslation();
return (
<>
<Form.Item
label="Host"
label={t('Host')}
name={['payload', 'hostname']}
rules={[
{ required: true },
@ -19,7 +22,7 @@ export const MonitorTCP: React.FC = React.memo(() => {
<Input placeholder="example.com or 1.2.3.4" />
</Form.Item>
<Form.Item
label="Host"
label={t('Port')}
name={['payload', 'port']}
rules={[
{ required: true },

View File

@ -1,7 +1,7 @@
import { MonitorInfo } from '../../../../types';
export interface MonitorProvider {
label: string;
label: React.ReactNode;
name: string;
link?: (info: MonitorInfo) => React.ReactNode;
form: React.ComponentType;

View File

@ -5,10 +5,12 @@ import { useCurrentWorkspaceId } from '../../store/user';
import { ColorTag } from '../ColorTag';
import { useNavigate } from 'react-router';
import { PlusOutlined } from '@ant-design/icons';
import { useTranslation } from '@i18next-toolkit/react';
interface NotificationPickerProps extends SelectProps<string> {}
export const NotificationPicker: React.FC<NotificationPickerProps> = React.memo(
(props) => {
const { t } = useTranslation();
const workspaceId = useCurrentWorkspaceId();
const navigate = useNavigate();
const { data: allNotification = [] } = trpc.notification.all.useQuery({
@ -21,12 +23,12 @@ export const NotificationPicker: React.FC<NotificationPickerProps> = React.memo(
<Empty
description={
<div className="py-2">
<div className="mb-1">Not found any notification</div>
<div className="mb-1">{t('Not found any notification')}</div>
<Button
icon={<PlusOutlined />}
onClick={() => navigate('/settings/notifications')}
>
Create Now
{t('Create Now')}
</Button>
</div>
}

View File

@ -2,6 +2,7 @@ import { Tag } from 'antd';
import React from 'react';
import { formatNumber } from '../../utils/common';
import { useGlobalStateStore } from '../../store/global';
import { useTranslation } from '@i18next-toolkit/react';
interface MetricCardProps {
value?: number;
@ -22,6 +23,7 @@ export const MetricCard: React.FC<MetricCardProps> = React.memo((props) => {
format = formatNumber,
hideComparison = false,
} = props;
const { t } = useTranslation();
const showPreviousPeriod = useGlobalStateStore(
(state) => state.showPreviousPeriod
);
@ -47,7 +49,7 @@ export const MetricCard: React.FC<MetricCardProps> = React.memo((props) => {
{format(prev)}
</div>
<div className="flex items-center whitespace-nowrap font-bold">
<span className="mr-2">Previous {label}</span>
<span className="mr-2">{t('Previous {{label}}', { label })} </span>
</div>
</div>
)}

View File

@ -42,7 +42,8 @@ export const MetricsTable: React.FC<MetricsTableProps> = React.memo((props) => {
title: title[0],
dataIndex: 'x',
ellipsis: true,
render: (val) => val ?? <span className="italic opacity-60">(None)</span>,
render: (val) =>
val ?? <span className="italic opacity-60">{t('(None)')}</span>,
},
{
title: title[1],

View File

@ -17,8 +17,10 @@ import {
import { useQueryClient } from '@tanstack/react-query';
import { useEvent } from '../../hooks/useEvent';
import { hostnameValidator } from '../../utils/validator';
import { useTranslation } from '@i18next-toolkit/react';
export const WebsiteInfo: React.FC = React.memo(() => {
const { t } = useTranslation();
const workspaceId = useCurrentWorkspaceId();
const { websiteId } = useParams<{
websiteId: string;
@ -54,7 +56,7 @@ export const WebsiteInfo: React.FC = React.memo(() => {
const [, handleDeleteWebsite] = useRequest(async () => {
await deleteWorkspaceWebsite(workspaceId, websiteId!);
message.success('Delete Success');
message.success(t('Delete Success'));
navigate('/settings/websites');
});
@ -78,7 +80,7 @@ export const WebsiteInfo: React.FC = React.memo(() => {
return (
<div>
<div className="h-24 flex items-center">
<div className="text-2xl flex-1">Website Info</div>
<div className="text-2xl flex-1">{t('Website Info')}</div>
</div>
<div>
@ -94,14 +96,18 @@ export const WebsiteInfo: React.FC = React.memo(() => {
}}
onFinish={handleSave}
>
<Form.Item label="Website ID" name="id">
<Form.Item label={t('Website ID')} name="id">
<Input size="large" disabled={true} />
</Form.Item>
<Form.Item label="Name" name="name" rules={[{ required: true }]}>
<Form.Item
label={t('Name')}
name="name"
rules={[{ required: true }]}
>
<Input size="large" />
</Form.Item>
<Form.Item
label="Domain"
label={t('Domain')}
name="domain"
rules={[
{ required: true },
@ -114,16 +120,18 @@ export const WebsiteInfo: React.FC = React.memo(() => {
</Form.Item>
<Form.Item
label="Monitor"
label={t('Monitor')}
name="monitorId"
tooltip="You can bind a monitor which will display health status in website overview"
tooltip={t(
'You can bind a monitor which will display health status in website overview'
)}
>
<MonitorPicker size="large" allowClear={true} />
</Form.Item>
<Form.Item>
<Button size="large" htmlType="submit">
Save
{t('Save')}
</Button>
</Form.Item>
</Form>
@ -131,11 +139,11 @@ export const WebsiteInfo: React.FC = React.memo(() => {
<Tabs.TabPane key={'data'} tab={'Data'}>
<Popconfirm
title="Delete Website"
title={t('Delete Website')}
onConfirm={() => handleDeleteWebsite()}
>
<Button type="primary" danger={true}>
Delete Website
{t('Delete Website')}
</Button>
</Popconfirm>
</Tabs.TabPane>

View File

@ -16,8 +16,10 @@ import { PageHeader } from '../PageHeader';
import { ModalButton } from '../ModalButton';
import { hostnameValidator } from '../../utils/validator';
import { trpc } from '../../api/trpc';
import { useTranslation } from '@i18next-toolkit/react';
export const WebsiteList: React.FC = React.memo(() => {
const { t } = useTranslation();
const [isModalOpen, setIsModalOpen] = useState(false);
const workspaceId = useCurrentWorkspaceId();
const [form] = Form.useForm();
@ -44,7 +46,7 @@ export const WebsiteList: React.FC = React.memo(() => {
return (
<div>
<PageHeader
title="Websites"
title={t('Websites')}
action={
<div>
<Button
@ -53,7 +55,7 @@ export const WebsiteList: React.FC = React.memo(() => {
size="large"
onClick={() => setIsModalOpen(true)}
>
Add Website
{t('Add Website')}
</Button>
</div>
}
@ -62,7 +64,7 @@ export const WebsiteList: React.FC = React.memo(() => {
<WebsiteListTable workspaceId={workspaceId} />
<Modal
title="Add Website"
title={t('Add Website')}
open={isModalOpen}
okButtonProps={{
loading: addWebsiteMutation.isLoading,
@ -72,17 +74,17 @@ export const WebsiteList: React.FC = React.memo(() => {
>
<Form layout="vertical" form={form}>
<Form.Item
label="Website Name"
label={t('Website Name')}
name="name"
tooltip="Website Name to Display"
tooltip={t('Website Name to Display')}
rules={[{ required: true }]}
>
<Input />
</Form.Item>
<Form.Item
label="Domain"
label={t('Domain')}
name="domain"
tooltip="Your server domain, or ip."
tooltip={t('Your server domain, or ip.')}
rules={[
{ required: true },
{
@ -105,6 +107,7 @@ const WebsiteListTable: React.FC<{ workspaceId: string }> = React.memo(
workspaceId: props.workspaceId,
});
const navigate = useNavigate();
const { t } = useTranslation();
const handleEdit = useEvent((websiteId) => {
navigate(`/settings/website/${websiteId}`);
@ -114,11 +117,11 @@ const WebsiteListTable: React.FC<{ workspaceId: string }> = React.memo(
return [
{
dataIndex: 'name',
title: 'Name',
title: t('Name'),
},
{
dataIndex: 'domain',
title: 'Domain',
title: t('Domain'),
},
{
key: 'action',
@ -135,9 +138,9 @@ const WebsiteListTable: React.FC<{ workspaceId: string }> = React.memo(
modalProps={{
children: (
<div>
<div>Tracking code</div>
<div>{t('Tracking code')}</div>
<div className="text-sm opacity-60">
Add this code into your website head script
{t('Add this code into your website head script')}
</div>
<Typography.Paragraph
copyable={{
@ -156,7 +159,7 @@ const WebsiteListTable: React.FC<{ workspaceId: string }> = React.memo(
icon={<EditOutlined />}
onClick={() => handleEdit(record.id)}
>
Edit
{t('Edit')}
</Button>
<Button
icon={<BarChartOutlined />}
@ -164,7 +167,7 @@ const WebsiteListTable: React.FC<{ workspaceId: string }> = React.memo(
navigate(`/website/${record.id}`);
}}
>
View
{t('View')}
</Button>
</div>
);

View File

@ -1,10 +1,12 @@
import React from 'react';
import { trpc } from '../../api/trpc';
import { useTranslation } from '@i18next-toolkit/react';
export const WebsiteOnlineCount: React.FC<{
workspaceId: string;
websiteId: string;
}> = React.memo((props) => {
const { t } = useTranslation();
const { workspaceId, websiteId } = props;
const { data: count } = trpc.website.onlineCount.useQuery({
@ -16,7 +18,9 @@ export const WebsiteOnlineCount: React.FC<{
return (
<div className="flex items-center space-x-2">
<div className="w-2.5 h-2.5 rounded-full bg-green-500" />
<span>{count} current visitor</span>
<span>
{count} {t('current visitor')}
</span>
</div>
);
}

View File

@ -24,12 +24,14 @@ import { useNavigate } from 'react-router';
import { AppRouterOutput, trpc } from '../../api/trpc';
import { getUserTimezone } from '../../api/model/user';
import { useGlobalStateStore } from '../../store/global';
import { useTranslation } from '@i18next-toolkit/react';
export const WebsiteOverview: React.FC<{
website: WebsiteInfo;
showDateFilter?: boolean;
actions?: React.ReactNode;
}> = React.memo((props) => {
const { t } = useTranslation();
const { website, showDateFilter = false, actions } = props;
const { startDate, endDate, unit, refresh } = useGlobalRangeDate();
const navigate = useNavigate();
@ -68,7 +70,7 @@ export const WebsiteOverview: React.FC<{
await Promise.all([refetchPageview(), refetchStats()]);
message.success('Refreshed');
message.success(t('Refreshed'));
});
const chartData = useMemo(() => {
@ -127,7 +129,7 @@ export const WebsiteOverview: React.FC<{
})
}
/>
<span className="ml-1">Previous period</span>
<span className="ml-1">{t('Previous period')}</span>
</div>
<Button
@ -155,6 +157,7 @@ WebsiteOverview.displayName = 'WebsiteOverview';
export const MetricsBar: React.FC<{
stats: AppRouterOutput['website']['stats'];
}> = React.memo((props) => {
const { t } = useTranslation();
const { pageviews, uniques, bounces, totaltime } = props.stats || {};
const bouncesNum = Math.min(uniques.value, bounces.value) / uniques.value;
const prevBouncesNum = Math.min(uniques.prev, bounces.prev) / uniques.prev;
@ -162,19 +165,19 @@ export const MetricsBar: React.FC<{
return (
<div className="flex gap-5 flex-wrap w-full">
<MetricCard
label="views"
label={t('views')}
value={pageviews.value}
prev={pageviews.prev}
change={pageviews.value - pageviews.prev}
/>
<MetricCard
label="visitors"
label={t('visitors')}
value={uniques.value}
prev={uniques.prev}
change={uniques.value - uniques.prev}
/>
<MetricCard
label="bounce rate"
label={t('bounce rate')}
reverseColors={true}
value={uniques.value ? bouncesNum * 100 : 0}
prev={uniques.prev ? prevBouncesNum * 100 : 0}
@ -186,7 +189,7 @@ export const MetricsBar: React.FC<{
format={(n) => formatNumber(n) + '%'}
/>
<MetricCard
label="average visit time"
label={t('average visit time')}
value={
totaltime.value && pageviews.value
? totaltime.value / (pageviews.value - bounces.value)

View File

@ -1,22 +1,17 @@
import React from 'react';
import { AppRouterOutput } from '../../../api/trpc';
import {
MapContainer,
CircleMarker,
Popup,
TileLayer,
useMap,
} from 'react-leaflet';
import { MapContainer, CircleMarker, Popup, TileLayer } from 'react-leaflet';
import { mapCenter } from './utils';
import 'leaflet/dist/leaflet.css';
import './VisitorLeafletMap.css';
import { useTranslation } from '@i18next-toolkit/react';
export const UserDataPoint: React.FC<{
longitude: number;
latitude: number;
count: number;
}> = React.memo((props) => {
const map = useMap();
const { t } = useTranslation();
return (
<CircleMarker
@ -30,7 +25,11 @@ export const UserDataPoint: React.FC<{
fillColor="rgb(236,112,20)"
fillOpacity={0.8}
>
<Popup>{props.count} users</Popup>
<Popup>
{t('{{num}} users', {
num: props.count,
})}
</Popup>
</CircleMarker>
);
});

View File

@ -4,6 +4,7 @@ import { useMemo, useReducer } from 'react';
import { getMinimumUnit } from '@tianji/shared';
import { DateRange, useGlobalStateStore } from '../store/global';
import { DateUnit } from '../utils/date';
import { useTranslation } from '@i18next-toolkit/react';
export function useGlobalRangeDate(): {
label: React.ReactNode;
@ -18,6 +19,7 @@ export function useGlobalRangeDate(): {
endDate: globalEndDate,
} = useGlobalStateStore();
const [updateInc, refresh] = useReducer((state: number) => state + 1, 0);
const { t } = useTranslation();
const { label, startDate, endDate, unit } = useMemo(() => {
if (dateRange === DateRange.Custom) {
@ -45,7 +47,7 @@ export function useGlobalRangeDate(): {
if (dateRange === DateRange.Today) {
return {
label: 'Today',
label: t('Today'),
startDate: dayjs().startOf('day'),
endDate: dayjs().endOf('day'),
unit: 'hour' as const,
@ -54,7 +56,7 @@ export function useGlobalRangeDate(): {
if (dateRange === DateRange.Yesterday) {
return {
label: 'Yesterday',
label: t('Yesterday'),
startDate: dayjs().subtract(1, 'day').startOf('day'),
endDate: dayjs().subtract(1, 'day').endOf('day'),
unit: 'hour' as const,
@ -63,7 +65,7 @@ export function useGlobalRangeDate(): {
if (dateRange === DateRange.ThisWeek) {
return {
label: 'This week',
label: t('This week'),
startDate: dayjs().startOf('week'),
endDate: dayjs().endOf('week'),
unit: 'day' as const,
@ -72,7 +74,7 @@ export function useGlobalRangeDate(): {
if (dateRange === DateRange.Last7Days) {
return {
label: 'Last 7 days',
label: t('Last 7 days'),
startDate: dayjs().subtract(7, 'day').startOf('day'),
endDate: dayjs().endOf('day'),
unit: 'day' as const,
@ -81,7 +83,7 @@ export function useGlobalRangeDate(): {
if (dateRange === DateRange.ThisMonth) {
return {
label: 'This month',
label: t('This month'),
startDate: dayjs().startOf('month'),
endDate: dayjs().endOf('month'),
unit: 'day' as const,
@ -90,7 +92,7 @@ export function useGlobalRangeDate(): {
if (dateRange === DateRange.Last30Days) {
return {
label: 'Last 30 days',
label: t('Last 30 days'),
startDate: dayjs().subtract(30, 'day').startOf('day'),
endDate: dayjs().endOf('day'),
unit: 'day' as const,
@ -99,7 +101,7 @@ export function useGlobalRangeDate(): {
if (dateRange === DateRange.Last90Days) {
return {
label: 'Last 90 days',
label: t('Last 90 days'),
startDate: dayjs().subtract(90, 'day').startOf('day'),
endDate: dayjs().endOf('day'),
unit: 'day' as const,
@ -108,7 +110,7 @@ export function useGlobalRangeDate(): {
if (dateRange === DateRange.ThisYear) {
return {
label: 'This year',
label: t('This year'),
startDate: dayjs().startOf('year'),
endDate: dayjs().endOf('year'),
unit: 'month' as const,
@ -117,7 +119,7 @@ export function useGlobalRangeDate(): {
// default last 24 hour
return {
label: 'Last 24 hours',
label: t('Last 24 hours'),
startDate: dayjs().subtract(1, 'day'),
endDate: dayjs(),
unit: 'hour' as const,

View File

@ -0,0 +1,44 @@
import type { I18nextToolkitConfig } from '@i18next-toolkit/cli';
export default {
locales: ['en', 'zh', 'jp', 'fr', 'de', 'ru'],
scanner: {
verbose: false,
autoImport: false,
ignoreText: [
'Tianji',
'(25, 587)',
'TLS (465)',
'https://github.com/caronc/apprise/wiki#notification-services',
'Slug',
'--',
'a-z',
'0-9',
'80',
'example.com or 1.2.3.4',
'TCP Port',
'OpenAI',
'sess-************',
'Ping',
'For example:&#13;&#10;{ "key": "value" }',
'Body',
'Headers',
'Content-Type',
'Method',
'https://example.com',
'HTTP',
'text/xml',
'application/x-www-form-urlencoded',
'application/json',
'OPTIONS',
'HEAD',
'DELETE',
'PATCH',
'PUT',
'POST',
'GET',
'HH:mm',
'YYYY-MM-DD HH:mm',
],
},
} satisfies I18nextToolkitConfig;

View File

@ -21,6 +21,7 @@ a {
#root, .App {
height: 100%;
font-family: 'gg sans', 'Noto Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif;
}
.App.dark {

View File

@ -7,6 +7,8 @@
"scripts": {
"dev": "vite --port 10000",
"build": "vite build",
"translation:extract": "i18next-toolkit extract",
"translation:scan": "i18next-toolkit scan",
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
@ -16,6 +18,7 @@
"@ant-design/icons": "^5.2.6",
"@antv/l7": "^2.20.14",
"@antv/larkmap": "^1.4.13",
"@i18next-toolkit/react": "^1.0.4",
"@loadable/component": "^5.16.3",
"@monaco-editor/react": "^4.6.0",
"@tanstack/react-query": "4.33.0",
@ -52,6 +55,7 @@
"zustand": "^4.4.1"
},
"devDependencies": {
"@i18next-toolkit/cli": "1.0.1",
"@types/leaflet": "^1.9.8",
"@types/loadable__component": "^5.13.8",
"@types/lodash-es": "^4.17.12",

View File

@ -10,6 +10,8 @@ import { ColorSchemeSwitcher } from '../components/ColorSchemeSwitcher';
import { version } from '@tianji/shared';
import { useIsMobile } from '../hooks/useIsMobile';
import { RiMenuUnfoldLine } from 'react-icons/ri';
import { useTranslation } from '@i18next-toolkit/react';
import { LanguageSelector } from '../components/LanguageSelector';
export const Layout: React.FC = React.memo(() => {
const [params] = useSearchParams();
@ -31,6 +33,7 @@ export const Layout: React.FC = React.memo(() => {
const isMobile = useIsMobile();
const showHeader = !params.has('hideHeader');
const navigate = useNavigate();
const { t } = useTranslation();
const accountEl = (
<Dropdown
@ -39,7 +42,7 @@ export const Layout: React.FC = React.memo(() => {
items: [
{
key: 'workspaces',
label: 'Workspaces',
label: t('Workspaces'),
children: workspaces.map((w) => ({
key: w.id,
label: `${w.name}${w.current ? '(current)' : ''}`,
@ -48,14 +51,14 @@ export const Layout: React.FC = React.memo(() => {
},
{
key: 'settings',
label: 'Settings',
label: t('Settings'),
onClick: () => {
navigate('/settings');
},
},
{
key: 'logout',
label: 'Logout',
label: t('Logout'),
onClick: () => {
logout();
},
@ -96,27 +99,27 @@ export const Layout: React.FC = React.memo(() => {
<div className="flex-1">
<MobileNavItem
to="/dashboard"
label="Dashboard"
label={t('Dashboard')}
onClick={() => setOpenDraw(false)}
/>
<MobileNavItem
to="/monitor"
label="Monitor"
label={t('Monitor')}
onClick={() => setOpenDraw(false)}
/>
<MobileNavItem
to="/website"
label="Website"
label={t('Website')}
onClick={() => setOpenDraw(false)}
/>
<MobileNavItem
to="/servers"
label="Servers"
label={t('Servers')}
onClick={() => setOpenDraw(false)}
/>
<MobileNavItem
to="/settings"
label="Settings"
label={t('Settings')}
onClick={() => setOpenDraw(false)}
/>
</div>
@ -140,16 +143,18 @@ export const Layout: React.FC = React.memo(() => {
{!isMobile && (
<>
<div className="flex gap-8">
<NavItem to="/dashboard" label="Dashboard" />
<NavItem to="/monitor" label="Monitor" />
<NavItem to="/website" label="Website" />
<NavItem to="/servers" label="Servers" />
<NavItem to="/settings" label="Settings" />
<NavItem to="/dashboard" label={t('Dashboard')} />
<NavItem to="/monitor" label={t('Monitor')} />
<NavItem to="/website" label={t('Website')} />
<NavItem to="/servers" label={t('Servers')} />
<NavItem to="/settings" label={t('Settings')} />
</div>
<div className="flex-1" />
<div className="flex gap-2">
<LanguageSelector />
<ColorSchemeSwitcher />
{accountEl}

View File

@ -6,9 +6,11 @@ import { trpc } from '../api/trpc';
import { setJWT } from '../api/auth';
import { setUserInfo } from '../store/user';
import { useGlobalConfig } from '../hooks/useConfig';
import { useTranslation } from '@i18next-toolkit/react';
export const Login: React.FC = React.memo(() => {
const navigate = useNavigate();
const { t } = useTranslation();
const mutation = trpc.user.login.useMutation();
const [{ loading }, handleLogin] = useRequest(async (values: any) => {
@ -34,14 +36,14 @@ export const Login: React.FC = React.memo(() => {
</Typography.Title>
<Form layout="vertical" disabled={loading} onFinish={handleLogin}>
<Form.Item
label="Username"
label={t('Username')}
name="username"
rules={[{ required: true }]}
>
<Input size="large" />
</Form.Item>
<Form.Item
label="Password"
label={t('Password')}
name="password"
rules={[{ required: true }]}
>
@ -55,7 +57,7 @@ export const Login: React.FC = React.memo(() => {
block={true}
loading={loading}
>
Login
{t('Login')}
</Button>
</Form.Item>
@ -69,7 +71,7 @@ export const Login: React.FC = React.memo(() => {
navigate('/register');
}}
>
Register
{t('Register')}
</Button>
</Form.Item>
)}

View File

@ -3,8 +3,10 @@ import { useCurrentWorkspaceId } from '../../store/user';
import { trpc } from '../../api/trpc';
import { Card } from 'antd';
import { MonitorEventList } from '../../components/monitor/MonitorEventList';
import { useTranslation } from '@i18next-toolkit/react';
export const MonitorOverview: React.FC = React.memo(() => {
const { t } = useTranslation();
const currentWorkspaceId = useCurrentWorkspaceId()!;
const { data: monitors = [] } = trpc.monitor.all.useQuery({
workspaceId: currentWorkspaceId,
@ -14,11 +16,11 @@ export const MonitorOverview: React.FC = React.memo(() => {
<div className="px-2">
<div className="grid gap-4 grid-cols-2">
<Card hoverable={true}>
<div>Monitors</div>
<div>{t('Monitors')}</div>
<div className="text-2xl font-semibold">{monitors.length}</div>
</Card>
<Card hoverable={true}>
<div>Available</div>
<div>{t('Available')}</div>
<div className="text-2xl font-semibold">
{monitors.filter((m) => m.active).length}
</div>

View File

@ -5,8 +5,10 @@ import { trpc } from '../../api/trpc';
import { Button, Card, Popconfirm } from 'antd';
import { DeleteOutlined, EditOutlined, EyeOutlined } from '@ant-design/icons';
import { useEvent } from '../../hooks/useEvent';
import { useTranslation } from '@i18next-toolkit/react';
export const MonitorPageList: React.FC = React.memo(() => {
const { t } = useTranslation();
const workspaceId = useCurrentWorkspaceId();
const navigate = useNavigate();
const { data: pages = [], refetch } = trpc.monitor.getAllPages.useQuery({
@ -26,7 +28,7 @@ export const MonitorPageList: React.FC = React.memo(() => {
return (
<div className="px-8 py-4">
<Button type="primary" onClick={() => navigate('/monitor/pages/add')}>
New page
{t('New page')}
</Button>
<div className="mt-4 flex flex-col gap-2">
@ -36,7 +38,7 @@ export const MonitorPageList: React.FC = React.memo(() => {
<div className="flex-1">{p.title}</div>
<div className="flex gap-2">
<Popconfirm
title="Did you sure delete this page?"
title={t('Did you sure delete this page?')}
onConfirm={() => handleDeletePage(p.id)}
okButtonProps={{
danger: true,

View File

@ -8,8 +8,10 @@ import { MonitorOverview } from './Overview';
import { Button } from 'antd';
import { MonitorPageList } from './PageList';
import { MonitorPageAdd } from './PageAdd';
import { useTranslation } from '@i18next-toolkit/react';
export const MonitorPage: React.FC = React.memo(() => {
const { t } = useTranslation();
const navigate = useNavigate();
return (
@ -21,14 +23,14 @@ export const MonitorPage: React.FC = React.memo(() => {
size="large"
onClick={() => navigate('/monitor/add')}
>
Add new Monitor
{t('Add new Monitor')}
</Button>
<Button
type="default"
size="large"
onClick={() => navigate('/monitor/pages')}
>
Pages
{t('Pages')}
</Button>
</div>
</div>

View File

@ -5,8 +5,10 @@ import { useRequest } from '../hooks/useRequest';
import { trpc } from '../api/trpc';
import { setJWT } from '../api/auth';
import { setUserInfo } from '../store/user';
import { useTranslation } from '@i18next-toolkit/react';
export const Register: React.FC = React.memo(() => {
const { t } = useTranslation();
const navigate = useNavigate();
const mutation = trpc.user.register.useMutation();
@ -29,18 +31,18 @@ export const Register: React.FC = React.memo(() => {
<img className="w-24 h-24" src="/icon.svg" />
</div>
<Typography.Title className="text-center" level={2}>
Register Account
{t('Register Account')}
</Typography.Title>
<Form layout="vertical" disabled={loading} onFinish={handleRegister}>
<Form.Item
label="Username"
label={t('Username')}
name="username"
rules={[{ required: true }]}
>
<Input size="large" />
</Form.Item>
<Form.Item
label="Password"
label={t('Password')}
name="password"
rules={[{ required: true }]}
>
@ -54,7 +56,7 @@ export const Register: React.FC = React.memo(() => {
block={true}
loading={loading}
>
Register
{t('Register')}
</Button>
</Form.Item>
</Form>

View File

@ -31,8 +31,10 @@ import clsx from 'clsx';
import { isServerOnline } from '@tianji/shared';
import { defaultErrorHandler, trpc } from '../api/trpc';
import { useRequest } from '../hooks/useRequest';
import { useTranslation } from '@i18next-toolkit/react';
export const Servers: React.FC = React.memo(() => {
const { t } = useTranslation();
const [isModalOpen, setIsModalOpen] = useState(false);
const [hideOfflineServer, setHideOfflineServer] = useState(false);
const workspaceId = useCurrentWorkspaceId();
@ -55,25 +57,25 @@ export const Servers: React.FC = React.memo(() => {
return (
<div>
<div className="h-24 flex items-center">
<div className="text-2xl flex-1">Servers</div>
<div className="text-2xl flex-1">{t('Servers')}</div>
<div className="flex items-center gap-4">
<div className="flex items-center gap-1 text-gray-500">
<Switch
checked={hideOfflineServer}
onChange={setHideOfflineServer}
/>
Hide Offline
{t('Hide Offline')}
</div>
<div>
<Popconfirm
title="Clear Offline Node"
description="Are you sure to clear all offline node?"
title={t('Clear Offline Node')}
description={t('Are you sure to clear all offline node?')}
disabled={loading}
onConfirm={handleClearOfflineNode}
>
<Button size="large" loading={loading}>
Clear Offline
{t('Clear Offline')}
</Button>
</Popconfirm>
</div>
@ -86,7 +88,7 @@ export const Servers: React.FC = React.memo(() => {
size="large"
onClick={() => setIsModalOpen(true)}
>
Add Server
{t('Add Server')}
</Button>
</div>
</div>
@ -94,7 +96,7 @@ export const Servers: React.FC = React.memo(() => {
<ServerList hideOfflineServer={hideOfflineServer} />
<Modal
title="Add Server"
title={t('Add Server')}
open={isModalOpen}
destroyOnClose={true}
okText="Done"
@ -106,12 +108,12 @@ export const Servers: React.FC = React.memo(() => {
items={[
{
key: 'auto',
label: 'Auto',
label: t('Auto'),
children: <InstallScript />,
},
{
key: 'manual',
label: 'Manual',
label: t('Manual'),
children: <AddServerStep />,
},
]}
@ -135,6 +137,7 @@ function useServerMap(): Record<string, ServerStatusInfo> {
export const ServerList: React.FC<{
hideOfflineServer: boolean;
}> = React.memo((props) => {
const { t } = useTranslation();
const serverMap = useServerMap();
const inc = useIntervalUpdate(2 * 1000);
const { hideOfflineServer } = props;
@ -158,16 +161,16 @@ export const ServerList: React.FC<{
return [
{
key: 'status',
title: 'Status',
title: t('Status'),
width: 90,
render: (val, record) => {
return isServerOnline(record) ? (
<Badge status="success" text="online" />
<Badge status="success" text={t('online')} />
) : (
<Tooltip
title={`Last online: ${dayjs(record.updatedAt).format(
'YYYY-MM-DD HH:mm:ss'
)}`}
title={t('Last online: {{time}}', {
time: dayjs(record.updatedAt).format('YYYY-MM-DD HH:mm:ss'),
})}
>
<Badge status="error" text="offline" />
</Tooltip>
@ -176,13 +179,13 @@ export const ServerList: React.FC<{
},
{
dataIndex: 'name',
title: 'Node Name',
title: t('Node Name'),
width: 150,
ellipsis: true,
},
{
dataIndex: 'hostname',
title: 'Host Name',
title: t('Host Name'),
width: 150,
ellipsis: true,
},
@ -192,18 +195,18 @@ export const ServerList: React.FC<{
// },
{
dataIndex: ['payload', 'uptime'],
title: 'Uptime',
title: t('Uptime'),
width: 150,
render: (val) => prettyMilliseconds(Number(val) * 1000),
},
{
dataIndex: ['payload', 'load'],
title: 'Load',
title: t('Load'),
width: 70,
},
{
key: 'nework',
title: 'Network',
title: t('Network'),
width: 110,
render: (_, record) => {
return (
@ -216,7 +219,7 @@ export const ServerList: React.FC<{
},
{
key: 'traffic',
title: 'Traffic',
title: t('Traffic'),
width: 130,
render: (_, record) => {
return (
@ -229,13 +232,13 @@ export const ServerList: React.FC<{
},
{
dataIndex: ['payload', 'cpu'],
title: 'CPU',
title: t('CPU'),
width: 80,
render: (val) => `${val}%`,
},
{
key: 'ram',
title: 'RAM',
title: t('RAM'),
width: 120,
render: (_, record) => {
return (
@ -248,7 +251,7 @@ export const ServerList: React.FC<{
},
{
key: 'hdd',
title: 'HDD',
title: t('HDD'),
width: 120,
render: (_, record) => {
return (
@ -261,7 +264,7 @@ export const ServerList: React.FC<{
},
{
dataIndex: 'updatedAt',
title: 'updatedAt',
title: t('updatedAt'),
width: 130,
render: (val) => {
return dayjs(val).format('MMM D HH:mm:ss');
@ -273,7 +276,9 @@ export const ServerList: React.FC<{
return (
<div>
<div className="text-right text-sm opacity-80">
Last updated at: {dayjs(lastUpdatedAt).format('YYYY-MM-DD HH:mm:ss')}
{t('Last updated at: {{date}}', {
date: dayjs(lastUpdatedAt).format('YYYY-MM-DD HH:mm:ss'),
})}
</div>
<div className="overflow-auto">
<Table
@ -281,7 +286,7 @@ export const ServerList: React.FC<{
columns={columns}
dataSource={dataSource}
pagination={false}
locale={{ emptyText: <Empty description="No server online" /> }}
locale={{ emptyText: <Empty description={t('No server online')} /> }}
rowClassName={(record) =>
clsx(!isServerOnline(record) && 'opacity-60')
}
@ -293,12 +298,13 @@ export const ServerList: React.FC<{
ServerList.displayName = 'ServerList';
export const InstallScript: React.FC = React.memo(() => {
const { t } = useTranslation();
const workspaceId = useCurrentWorkspaceId();
const command = `curl -o- ${window.location.origin}/serverStatus/${workspaceId}/install.sh?url=${window.location.origin} | bash`;
return (
<div>
<div>Run this command in your linux machine</div>
<div>{t('Run this command in your linux machine')}</div>
<Typography.Paragraph
copyable={{
@ -311,14 +317,16 @@ export const InstallScript: React.FC = React.memo(() => {
</Typography.Paragraph>
<div>
Or you wanna report server status in windows server? switch to Manual
tab
{t(
'Or you wanna report server status in windows server? switch to Manual tab'
)}
</div>
</div>
);
});
export const AddServerStep: React.FC = React.memo(() => {
const { t } = useTranslation();
const workspaceId = useCurrentWorkspaceId();
const [current, setCurrent] = useState(0);
const serverMap = useServerMap();
@ -348,10 +356,10 @@ export const AddServerStep: React.FC = React.memo(() => {
current={current}
items={[
{
title: 'Download Client Reportor',
title: t('Download Client Reportor'),
description: (
<div>
Download reporter from{' '}
{t('Download reporter from')}{' '}
<Typography.Link
href="https://github.com/msgbyte/tianji/releases"
target="_blank"
@ -362,16 +370,16 @@ export const AddServerStep: React.FC = React.memo(() => {
}
}}
>
Releases Page
{t('Releases Page')}
</Typography.Link>
</div>
),
},
{
title: 'Run',
title: t('Run'),
description: (
<div>
run reporter with:{' '}
{t('run reporter with')}:{' '}
<Typography.Text
code={true}
copyable={{ format: 'text/plain', text: command }}
@ -389,20 +397,20 @@ export const AddServerStep: React.FC = React.memo(() => {
}
}}
>
Next step
{t('Next step')}
</Button>
</div>
),
},
{
title: 'Waiting for receive UDP pack',
title: t('Waiting for receive UDP pack'),
description: (
<div>
{diffServerNames.length === 0 || checking === false ? (
<Loading />
) : (
<div>
Is this your servers?
{t('Is this your servers?')}
{diffServerNames.map((n) => (
<div key={n}>- {n}</div>
))}

View File

@ -8,8 +8,10 @@ import { last } from 'lodash-es';
import { useWatch } from '../../hooks/useWatch';
import { ColorTag } from '../../components/ColorTag';
import dayjs from 'dayjs';
import { useTranslation } from '@i18next-toolkit/react';
export const AuditLog: React.FC = React.memo(() => {
const { t } = useTranslation();
const workspaceId = useCurrentWorkspaceId();
const parentRef = useRef<HTMLDivElement>(null);
@ -53,7 +55,7 @@ export const AuditLog: React.FC = React.memo(() => {
return (
<div>
<PageHeader title="Audit Log" />
<PageHeader title={t('Audit Log')} />
<Card>
<List>
@ -81,9 +83,9 @@ export const AuditLog: React.FC = React.memo(() => {
>
{isLoaderRow ? (
hasNextPage ? (
'Loading more...'
t('Loading more...')
) : (
'Nothing more to load'
t('Nothing more to load')
)
) : (
<div className="flex items-center">

View File

@ -10,8 +10,10 @@ import { NoWorkspaceTip } from '../../components/NoWorkspaceTip';
import { PageHeader } from '../../components/PageHeader';
import { useEvent } from '../../hooks/useEvent';
import { useCurrentWorkspaceId } from '../../store/user';
import { useTranslation } from '@i18next-toolkit/react';
export const NotificationList: React.FC = React.memo(() => {
const { t } = useTranslation();
const [open, setOpen] = useState(false);
const currentWorkspaceId = useCurrentWorkspaceId();
const { data: list = [], refetch } = trpc.notification.all.useQuery({
@ -58,7 +60,7 @@ export const NotificationList: React.FC = React.memo(() => {
return (
<div>
<PageHeader
title="Notification List"
title={t('Notification List')}
action={
<div>
<Button
@ -67,7 +69,7 @@ export const NotificationList: React.FC = React.memo(() => {
size="large"
onClick={() => handleOpenModal()}
>
New
{t('New')}
</Button>
</div>
}
@ -90,10 +92,10 @@ export const NotificationList: React.FC = React.memo(() => {
});
}}
>
Edit
{t('Edit')}
</Button>,
<Popconfirm
title="Is delete this item?"
title={t('Is delete this item?')}
okButtonProps={{
danger: true,
}}

View File

@ -8,8 +8,10 @@ import {
trpc,
} from '../../api/trpc';
import { useLogout } from '../../api/model/user';
import { useTranslation } from '@i18next-toolkit/react';
export const Profile: React.FC = React.memo(() => {
const { t } = useTranslation();
const userInfo = useUserStore((state) => state.info);
const [openChangePassword, setOpenChangePassword] = useState(false);
@ -22,23 +24,23 @@ export const Profile: React.FC = React.memo(() => {
return (
<div>
<PageHeader title="Profile" />
<PageHeader title={t('Profile')} />
<Card>
<Form layout="vertical">
<Form.Item label="Current Workspace Id">
<Form.Item label={t('Current Workspace Id')}>
<Typography.Text copyable={true} code={true}>
{userInfo?.currentWorkspace?.id}
</Typography.Text>
</Form.Item>
<Form.Item label="User Id">
<Form.Item label={t('User Id')}>
<Typography.Text copyable={true} code={true}>
{userInfo?.id}
</Typography.Text>
</Form.Item>
<Form.Item label="Password">
<Form.Item label={t('Password')}>
<Button danger={true} onClick={() => setOpenChangePassword(true)}>
Change Password
{t('Change Password')}
</Button>
</Form.Item>
</Form>
@ -46,7 +48,7 @@ export const Profile: React.FC = React.memo(() => {
<Modal
open={openChangePassword}
title="Change password"
title={t('Change password')}
footer={null}
maskClosable={false}
onCancel={() => setOpenChangePassword(false)}
@ -64,21 +66,21 @@ export const Profile: React.FC = React.memo(() => {
}}
>
<Form.Item
label="Old Password"
label={t('Old Password')}
name="oldPassword"
rules={[{ required: true }]}
>
<Input.Password />
</Form.Item>
<Form.Item
label="New Password"
label={t('New Password')}
name="newPassword"
rules={[{ required: true }]}
>
<Input.Password />
</Form.Item>
<Form.Item
label="New Password Repeat"
label={t('New Password Repeat')}
name="newPasswordRepeat"
rules={[
{ required: true },
@ -88,7 +90,9 @@ export const Profile: React.FC = React.memo(() => {
return Promise.resolve();
}
return Promise.reject('The two passwords are not consistent');
return Promise.reject(
t('The two passwords are not consistent')
);
},
}),
]}
@ -101,7 +105,7 @@ export const Profile: React.FC = React.memo(() => {
htmlType="submit"
loading={changePasswordMutation.isLoading}
>
Submit
{t('Submit')}
</Button>
</Form.Item>
</Form>

View File

@ -7,23 +7,24 @@ import { useEvent } from '../../hooks/useEvent';
import { NotificationList } from './NotificationList';
import { Profile } from './Profile';
import { AuditLog } from './AuditLog';
import { Trans } from '@i18next-toolkit/react';
const items: MenuProps['items'] = [
{
key: 'websites',
label: 'Websites',
label: <Trans>Websites</Trans>,
},
{
key: 'notifications',
label: 'Notifications',
label: <Trans>Notifications</Trans>,
},
{
key: 'auditLog',
label: 'Audit Log',
label: <Trans>Audit Log</Trans>,
},
{
key: 'profile',
label: 'Profile',
label: <Trans>Profile</Trans>,
},
];

View File

@ -10,8 +10,10 @@ import { WebsiteOverview } from '../../components/website/WebsiteOverview';
import { useGlobalRangeDate } from '../../hooks/useGlobalRangeDate';
import { useCurrentWorkspaceId } from '../../store/user';
import { RightOutlined } from '@ant-design/icons';
import { useTranslation } from '@i18next-toolkit/react';
export const WebsiteDetail: React.FC = React.memo(() => {
const { t } = useTranslation();
const { websiteId } = useParams();
const workspaceId = useCurrentWorkspaceId();
const { data: website, isLoading } = trpc.website.info.useQuery({
@ -46,7 +48,7 @@ export const WebsiteDetail: React.FC = React.memo(() => {
<MetricsTable
websiteId={websiteId}
type="url"
title={['Pages', 'Views']}
title={[t('Pages'), t('Views')]}
startAt={startAt}
endAt={endAt}
/>
@ -55,7 +57,7 @@ export const WebsiteDetail: React.FC = React.memo(() => {
<MetricsTable
websiteId={websiteId}
type="referrer"
title={['Referrers', 'Views']}
title={[t('Referrers'), t('Views')]}
startAt={startAt}
endAt={endAt}
/>
@ -64,7 +66,7 @@ export const WebsiteDetail: React.FC = React.memo(() => {
<MetricsTable
websiteId={websiteId}
type="browser"
title={['Browser', 'Visitors']}
title={[t('Browser'), t('Visitors')]}
startAt={startAt}
endAt={endAt}
/>
@ -73,7 +75,7 @@ export const WebsiteDetail: React.FC = React.memo(() => {
<MetricsTable
websiteId={websiteId}
type="os"
title={['OS', 'Visitors']}
title={[t('OS'), t('Visitors')]}
startAt={startAt}
endAt={endAt}
/>
@ -82,7 +84,7 @@ export const WebsiteDetail: React.FC = React.memo(() => {
<MetricsTable
websiteId={websiteId}
type="device"
title={['Devices', 'Visitors']}
title={[t('Devices'), t('Visitors')]}
startAt={startAt}
endAt={endAt}
/>
@ -91,7 +93,7 @@ export const WebsiteDetail: React.FC = React.memo(() => {
<MetricsTable
websiteId={websiteId}
type="country"
title={['Countries', 'Visitors']}
title={[t('Countries'), t('Visitors')]}
startAt={startAt}
endAt={endAt}
/>
@ -102,14 +104,14 @@ export const WebsiteDetail: React.FC = React.memo(() => {
icon={<RightOutlined className="m-0" />}
onClick={() => navigate(`/website/${websiteId}/map`)}
>
Visitor Map
{t('Visitor Map')}
</Button>
</Card.Grid>
<Card.Grid hoverable={false} className="!w-1/2 min-h-[470px]">
<MetricsTable
websiteId={websiteId}
type="event"
title={['Events', 'Actions']}
title={[t('Events'), t('Actions')]}
startAt={startAt}
endAt={endAt}
/>

View File

@ -9,10 +9,12 @@ import { WebsiteVisitorMap } from '../../components/website/WebsiteVisitorMap';
import { DateFilter } from '../../components/DateFilter';
import { LeftOutlined } from '@ant-design/icons';
import { Button } from 'antd';
import { useTranslation } from '@i18next-toolkit/react';
export const WebsiteVisitorMapPage: React.FC = React.memo(() => {
const { websiteId } = useParams();
const workspaceId = useCurrentWorkspaceId();
const { t } = useTranslation();
const { data: website, isLoading } = trpc.website.info.useQuery({
workspaceId,
websiteId: websiteId!,
@ -40,7 +42,8 @@ export const WebsiteVisitorMapPage: React.FC = React.memo(() => {
onClick={() => navigate(`/website/${websiteId}`)}
/>
<div>
<span className="font-bold">{website.name}</span>'s visitor map
<span className="font-bold">{website.name}</span>
{t("'s visitor map")}
</div>
<DateFilter />
</div>

Binary file not shown.

After

Width:  |  Height:  |  Size: 269 B

View File

@ -0,0 +1,218 @@
{
"k10336b1": "Nichts mehr zu laden",
"k10c85f27": "TLS-Fehler ignorieren",
"k112a7174": "Nutzung",
"k1192dd43": "Bitte Arbeitsbereich auswählen",
"k11e887ab": "Dieses Element löschen?",
"k11f09c87": "Ansichten",
"k1286421": "Benutzername",
"k134e0d97": "Aktuell",
"k14c6c425": "Ergebnis",
"k158336d6": "Anfrage-Timeout(s)",
"k1598e726": "aktueller Besucher",
"k15ae36a6": "Browser",
"k16c8909f": "Anzeigename",
"k1777bbf2": "Manuell",
"k186365b3": "{{monitorName}}'s Diagramm",
"k1964b988": "Stopp",
"k1bd89236": "Reporter mit ausführen",
"k1c33c293": "Einstellungen",
"k1eb5b3ed": "Übersicht",
"k20edf271": "24h",
"k21077124": "Bearbeiten",
"k2264aac": "Dieses Jahr",
"k246063be": "Chat-ID",
"k2497052e": "Diagramm",
"k25b3dc00": "Skript JS-Code",
"k264de775": "Mehr laden...",
"k277e2626": "Verfügbar",
"k28059d49": "Aktuelle Arbeitsbereichs-ID",
"k2813d1f7": "Dieser Monat",
"k2a6a7d8f": "UNTEN",
"k2b2d40d4": "Testcode",
"k2c2712a4": "CPU",
"k2e6dbf02": "An E-Mail",
"k2ea8a019": "Monitor",
"k30b5f01b": "Arbeitsbereiche",
"k30e234ee": "Sie haben noch kein Dashboard-Element, bitte treten Sie in den Bearbeitungsmodus und fügen Sie Ihr Element hinzu.",
"k310fee": "Letzte 30 Tage",
"k32344f64": "Daten löschen",
"k3260f019": "Abmelden",
"k3471e956": "Neues Passwort wiederholen",
"k389db675": "Einreichen",
"k38c62888": "Bitte Monitor auswählen",
"k3cedb797": "Sicherheit",
"k3d3baf52": "durchschnittliche Besuchszeit",
"k3dbe79b1": "Löschen",
"k3de768a1": "Passwort ändern",
"k3e757ddf": "Hier gibt es noch keinen Monitor.",
"k43e21ee9": "Client-Reporter herunterladen",
"k44cad477": "(Aktuell)",
"k46f0adde": "Besucherkarte",
"k47f43dbc": "Gesundheit von {{monitorName}}",
"k4905ed7b": "KEINE",
"k490ada32": "Website hinzufügen",
"k49e5f1d2": "1w",
"k4ac4dd36": "Verweisende Seiten",
"k4de48e75": "Maximale Wiederholungen",
"k4e08cf58": "Detailnummer anzeigen",
"k4eea9393": "Profil",
"k506a90b2": "Ihr Server-Domain oder IP.",
"k517747e1": "Letzte 24 Stunden",
"k51bac044": "Abbrechen",
"k53ae02a5": "Neu",
"k542b527c": "Ereignisse",
"k58f90514": "Bot-Token",
"k593cf342": "Sind Sie sicher, diesen Monitor zu löschen?",
"k5a839f71": "Betriebszeit",
"k5eb87a8b": "Start",
"k5ecf04b0": "Ansicht",
"k6067f0ff": "TLS/SSL-Fehler ignorieren",
"k621317b5": "Neue Seite",
"k62e19375": "Letzte Aktualisierung: {{date}}",
"k646a3a80": "Metriken von {{monitorName}}",
"k646c0ae2": "HDD",
"k67c5a895": "Gestern",
"k683be220": "Ausführen",
"k691b7170": "Gestoppt",
"k698348ff": "Warten auf UDP-Paketempfang",
"k6acf5248": "Kürzlich",
"k6b36580f": "Apprise-URL",
"k6bc9e414": "Anmelden",
"k6f15bcc3": "Host",
"k717660a5": "RAM",
"k721589c1": "Heute",
"k74a240": "Gesundheitsbalken",
"k75581e13": "CC",
"k75bfaaa6": "Fügen Sie diesen Code in das Kopf-Skript Ihrer Website ein",
"k784dd132": "Test",
"k7927b824": "Sind Sie sicher, alle Offline-Knoten zu löschen?",
"k7ac44a6e": "Sitzungsschlüssel",
"k7b74a43f": "Besucher",
"k7c95e6a5": "Sind Sie sicher, diese Seite zu löschen?",
"k7cac602a": "Status",
"k7f01b47c": "Prüfprotokoll",
"k7f4bcf6b": "Monitore",
"k8037cc6b": "Server",
"k8202c669": "Besucher",
"k845abd5b": "Nächster Schritt",
"k84ce1618": "(24 Stunden)",
"k85344b23": "Laden",
"k85c5fd4c": "Noch kein Monitor eingerichtet",
"k8746ec38": "Monitor auswählen",
"k88a9bf01": "Abzeichen anzeigen",
"k88d2647b": "Website",
"k89056082": "(30 Tage)",
"k8a44833f": "Dienste",
"k8bac6ae0": "6h",
"k8ef56a20": "Maximale Wiederholungen, bevor der Dienst als ausgefallen markiert wird und eine Benachrichtigung gesendet wird",
"k8f8fbf6": "Teilen mit...",
"k8ff3a55a": "Warnung",
"k9022468f": "Offline löschen",
"k90873752": "Altes Passwort",
"k90a82c67": "Konto registrieren",
"k90b668e5": "Letzte 24 Stunden",
"k93374bc9": "Website löschen",
"k97ddb155": "Aktuelle Antwort anzeigen",
"k98f433ee": "Reporter herunterladen von",
"k9a272ecf": "Sind das Ihre Server?",
"k9a3cc801": "Zeichen akzeptieren",
"k9add1fac": "Verkehr",
"k9be2209c": "Anzuzeigender Website-Name",
"k9e32beea": "online",
"k9e759f8": "Benachrichtigungsliste",
"k9fa794aa": "Website-Name",
"ka0051b3d": "Domain",
"ka0ddbfb": "Website-Infos",
"ka2b9bc3c": "Vorherige Periode",
"ka2fae1c6": "Benutzer-ID",
"ka388d3bf": "Sie können einen Monitor verbinden, der den Gesundheitsstatus in der Website-Übersicht anzeigt",
"ka40aea11": "Oder möchten Sie den Serverstatus in einem Windows-Server melden? Wechseln Sie zum manuellen Tab",
"ka44150a0": "Letzte 90 Tage",
"ka68f2242": "Registrieren",
"ka6ee7455": "Website-ID",
"ka71c12e1": "Die beiden Passwörter stimmen nicht überein",
"ka765ad32": "Benachrichtigung",
"ka9d081ac": "Überprüfungsintervall(s)",
"kaa0788e9": "Layout erfolgreich gespeichert",
"kaa0ccaab": "Von E-Mail",
"kab56db46": "Herzschläge",
"kacbdae07": "Durchschn. Antwort",
"kadef6c48": "Vorherige {{label}}",
"kaf39be20": "Netzwerk",
"kb01f4f95": "Fertig",
"kb0e351e0": "Aktualisiert",
"kb320aac4": "Überwacht seit {{dayNum}} Tagen",
"kb5673707": "Letzte 7 Tage",
"kb659c1bc": "Zert. Ablauf",
"kb8de8c50": "BCC",
"kbb58c99c": "Erfolgreich gelöscht",
"kbcf67f53": "Neues Passwort",
"kbd1e7dee": "Nutzung: {{usage}}ms",
"kbd425e0e": "aktualisiertAm",
"kc00cf2c7": "Dies zeigt Ihr letztes Ergebnis Ihres Monitors an",
"kc0c6a913": "Knotenname",
"kc1f1f6c9": "Benachrichtigungstyp",
"kc45a417b": "BS",
"kc4910af7": "Passwort ändern",
"kc4ab7848": "Offline-Knoten löschen",
"kc4e91854": "Sind Sie sicher, dass Sie alle Herzschläge für diesen Monitor löschen möchten?",
"kc5573507": "Hinzufügen",
"kc5f82d53": "Zum Beispiel: pushdeer://pushKey",
"kc6888ac4": "Auto",
"kc6cac621": "(Keine)",
"kc70d69ad": "Antwort",
"kc9b446d1": "Ausführung abgeschlossen",
"kcacbfde1": "Jetzt erstellen",
"kcaf5c873": "Aktionen",
"kcb8fd4ce": "Kein Server online",
"kcbc00b39": "Wählen Sie Ihren Datumsbereich aus",
"kcc3b034e": "Url",
"kcc50957": "Neuen Monitor hinzufügen",
"kccaa732a": "Keine aufeinanderfolgenden Bindestriche",
"kccb42483": "Passwort",
"kd031b383": "Ansichten",
"kd211e2d4": "Versionsseite",
"kd37efb26": "Benachrichtigungen",
"kd46ab159": "Keine Ereignisse",
"kd7985726": "{{num}} Benutzer",
"kd92fa3e7": "Host-Name",
"kdaa949e5": "Ereignisse von {{monitorName}}",
"kdb61adbb": "Offline verbergen",
"kdc51b5db": "Websites",
"kde37bc27": "Zurück zum Admin",
"kde657d5b": "Dashboard",
"kdeba7706": "Geräte",
"kdf5da1d2": "Keine / STARTTLS",
"kdf97690e": "Länder",
"ke188f24b": "Absprungrate",
"ke1b5ca71": "Seiten",
"ke2fe505b": "Diese Woche",
"ke3a3f2f2": "Port",
"ke46232fe": "Server hinzufügen",
"ke5b015e9": "Sie können einen Token von https://t.me/BotFather erhalten.",
"ke6797c65": "Sie können Ihre Chat-ID erhalten, indem Sie eine Nachricht an den Bot senden und diese URL besuchen, um die chat_id zu sehen",
"ke9d2fef3": "Dieser Monat",
"ke9dcaa64": "Keine Benachrichtigung gefunden",
"keaf7576f": "Titel",
"ked37937b": "Metriken",
"ked7eea1a": "OBEN",
"ked8814bc": "Kopieren",
"kedc69eb6": "Tracking-Code",
"kef701e50": "Monitor hinzufügen",
"kf22813ad": "Benutzerdefiniert",
"kf3b749ef": "Unterstützt Direktchat / Gruppe / Kanal-Chat-ID",
"kf55495e0": "Speichern",
"kf5bbb568": "Besucherkarte von",
"kf6bc1610": "Monitortyp",
"kf6db9ea5": "3h",
"kf7d5dbf8": "Mehr lesen",
"kf97b6f71": "Führen Sie diesen Befehl auf Ihrer Linux-Maschine aus",
"kf9877f28": "Details anzeigen",
"kfc98929b": "{{num}} Tage",
"kfd33c459": "Kopieren erfolgreich!",
"kfdaf0bb3": "Zuletzt online: {{time}}",
"kfe11d138": "Name",
"kfedb6cd8": "Sind Sie sicher, dass Sie alle Ereignisse für diesen Monitor löschen möchten?",
"kff849f78": "Automatisch abrufen"
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 288 B

View File

@ -0,0 +1,218 @@
{
"k10336b1": "Nothing more to load",
"k10c85f27": "Ignore TLS Error",
"k112a7174": "Usage",
"k1192dd43": "Please Select Workspace",
"k11e887ab": "Is delete this item?",
"k11f09c87": "views",
"k1286421": "Username",
"k134e0d97": "Current",
"k14c6c425": "Result",
"k158336d6": "Request Timeout(s)",
"k1598e726": "current visitor",
"k15ae36a6": "Browser",
"k16c8909f": "Display Name",
"k1777bbf2": "Manual",
"k186365b3": "{{monitorName}}'s Chart",
"k1964b988": "Stop",
"k1bd89236": "run reporter with",
"k1c33c293": "Settings",
"k1eb5b3ed": "Overview",
"k20edf271": "24h",
"k21077124": "Edit",
"k2264aac": "This year",
"k246063be": "Chat ID",
"k2497052e": "Chart",
"k25b3dc00": "Script JS Code",
"k264de775": "Loading more...",
"k277e2626": "Available",
"k28059d49": "Current Workspace Id",
"k2813d1f7": "This Month",
"k2a6a7d8f": "DOWN",
"k2b2d40d4": "Test Code",
"k2c2712a4": "CPU",
"k2e6dbf02": "To Email",
"k2ea8a019": "Monitor",
"k30b5f01b": "Workspaces",
"k30e234ee": "You have not dashboard item yet, please enter edit mode and add you item.",
"k310fee": "Last 30 days",
"k32344f64": "Clear Data",
"k3260f019": "Logout",
"k3471e956": "New Password Repeat",
"k389db675": "Submit",
"k38c62888": "Please select monitor",
"k3cedb797": "Security",
"k3d3baf52": "average visit time",
"k3dbe79b1": "Delete",
"k3de768a1": "Change password",
"k3e757ddf": "Here is no monitor yet.",
"k43e21ee9": "Download Client Reportor",
"k44cad477": "(Current)",
"k46f0adde": "Visitor Map",
"k47f43dbc": "{{monitorName}}'s Health",
"k4905ed7b": "NONE",
"k490ada32": "Add Website",
"k49e5f1d2": "1w",
"k4ac4dd36": "Referrers",
"k4de48e75": "Max Retries",
"k4e08cf58": "Show Detail Number",
"k4eea9393": "Profile",
"k506a90b2": "Your server domain, or ip.",
"k517747e1": "Last 24 hours",
"k51bac044": "Cancel",
"k53ae02a5": "New",
"k542b527c": "Events",
"k58f90514": "Bot Token",
"k593cf342": "Did you sure delete this monitor?",
"k5a839f71": "Uptime",
"k5eb87a8b": "Start",
"k5ecf04b0": "View",
"k6067f0ff": "Ignore TLS/SSL error",
"k621317b5": "New page",
"k62e19375": "Last updated at: {{date}}",
"k646a3a80": "{{monitorName}}'s Metrics",
"k646c0ae2": "HDD",
"k67c5a895": "Yesterday",
"k683be220": "Run",
"k691b7170": "Stopped",
"k698348ff": "Waiting for receive UDP pack",
"k6acf5248": "Recent",
"k6b36580f": "Apprise URL",
"k6bc9e414": "Login",
"k6f15bcc3": "Host",
"k717660a5": "RAM",
"k721589c1": "Today",
"k74a240": "Health Bar",
"k75581e13": "CC",
"k75bfaaa6": "Add this code into your website head script",
"k784dd132": "Test",
"k7927b824": "Are you sure to clear all offline node?",
"k7ac44a6e": "Session Key",
"k7b74a43f": "visitors",
"k7c95e6a5": "Did you sure delete this page?",
"k7cac602a": "Status",
"k7f01b47c": "Audit Log",
"k7f4bcf6b": "Monitors",
"k8037cc6b": "Servers",
"k8202c669": "Visitors",
"k845abd5b": "Next step",
"k84ce1618": "(24 hour)",
"k85344b23": "Load",
"k85c5fd4c": "No any monitor has been set",
"k8746ec38": "Select monitor",
"k88a9bf01": "Show Badge",
"k88d2647b": "Website",
"k89056082": "(30 days)",
"k8a44833f": "Services",
"k8bac6ae0": "6h",
"k8ef56a20": "Maximum retries before the service is marked as down and a notification is sent",
"k8f8fbf6": "Share with...",
"k8ff3a55a": "Warning",
"k9022468f": "Clear Offline",
"k90873752": "Old Password",
"k90a82c67": "Register Account",
"k90b668e5": "Last 24 Hours",
"k93374bc9": "Delete Website",
"k97ddb155": "Show Current Response",
"k98f433ee": "Download reporter from",
"k9a272ecf": "Is this your servers?",
"k9a3cc801": "Accept characters",
"k9add1fac": "Traffic",
"k9be2209c": "Website Name to Display",
"k9e32beea": "online",
"k9e759f8": "Notification List",
"k9fa794aa": "Website Name",
"ka0051b3d": "Domain",
"ka0ddbfb": "Website Info",
"ka2b9bc3c": "Previous period",
"ka2fae1c6": "User Id",
"ka388d3bf": "You can bind a monitor which will display health status in website overview",
"ka40aea11": "Or you wanna report server status in windows server? switch to Manual tab",
"ka44150a0": "Last 90 days",
"ka68f2242": "Register",
"ka6ee7455": "Website ID",
"ka71c12e1": "The two passwords are not consistent",
"ka765ad32": "Notification",
"ka9d081ac": "Check Interval(s)",
"kaa0788e9": "Layout saved success",
"kaa0ccaab": "From Email",
"kab56db46": "Heartbeats",
"kacbdae07": "Avg. Response",
"kadef6c48": "Previous {{label}}",
"kaf39be20": "Network",
"kb01f4f95": "Done",
"kb0e351e0": "Refreshed",
"kb320aac4": "Monitored for {{dayNum}} days",
"kb5673707": "Last 7 days",
"kb659c1bc": "Cert Exp.",
"kb8de8c50": "BCC",
"kbb58c99c": "Delete Success",
"kbcf67f53": "New Password",
"kbd1e7dee": "Usage: {{usage}}ms",
"kbd425e0e": "updatedAt",
"kc00cf2c7": "This will show your recent result of your monitor",
"kc0c6a913": "Node Name",
"kc1f1f6c9": "Notification Type",
"kc45a417b": "OS",
"kc4910af7": "Change Password",
"kc4ab7848": "Clear Offline Node",
"kc4e91854": "Are you sure want to delete all heartbeats for this monitor?",
"kc5573507": "Add",
"kc5f82d53": "For example: pushdeer://pushKey",
"kc6888ac4": "Auto",
"kc6cac621": "(None)",
"kc70d69ad": "Response",
"kc9b446d1": "Run Completed",
"kcacbfde1": "Create Now",
"kcaf5c873": "Actions",
"kcb8fd4ce": "No server online",
"kcbc00b39": "Select your date range",
"kcc3b034e": "Url",
"kcc50957": "Add new Monitor",
"kccaa732a": "No consecutive dashes",
"kccb42483": "Password",
"kd031b383": "Views",
"kd211e2d4": "Releases Page",
"kd37efb26": "Notifications",
"kd46ab159": "No events",
"kd7985726": "{{num}} users",
"kd92fa3e7": "Host Name",
"kdaa949e5": "{{monitorName}}'s Events",
"kdb61adbb": "Hide Offline",
"kdc51b5db": "Websites",
"kde37bc27": "Back to Admin",
"kde657d5b": "Dashboard",
"kdeba7706": "Devices",
"kdf5da1d2": "None / STARTTLS",
"kdf97690e": "Countries",
"ke188f24b": "bounce rate",
"ke1b5ca71": "Pages",
"ke2fe505b": "This week",
"ke3a3f2f2": "Port",
"ke46232fe": "Add Server",
"ke5b015e9": "You can get a token from https://t.me/BotFather.",
"ke6797c65": "You can get your chat ID by sending a message to the bot and going to this URL to view the chat_id",
"ke9d2fef3": "This month",
"ke9dcaa64": "Not found any notification",
"keaf7576f": "Title",
"ked37937b": "Metrics",
"ked7eea1a": "UP",
"ked8814bc": "Copy",
"kedc69eb6": "Tracking code",
"kef701e50": "Add Monitor",
"kf22813ad": "Custom",
"kf3b749ef": "Support Direct Chat / Group / Channel's Chat ID",
"kf55495e0": "Save",
"kf5bbb568": "'s visitor map",
"kf6bc1610": "Monitor Type",
"kf6db9ea5": "3h",
"kf7d5dbf8": "Read more",
"kf97b6f71": "Run this command in your linux machine",
"kf9877f28": "View Details",
"kfc98929b": "{{num}} days",
"kfd33c459": "Copy success!",
"kfdaf0bb3": "Last online: {{time}}",
"kfe11d138": "Name",
"kfedb6cd8": "Are you sure want to delete all events for this monitor?",
"kff849f78": "Auto Fetch"
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 267 B

View File

@ -0,0 +1,218 @@
{
"k10336b1": "Plus rien à charger",
"k10c85f27": "Ignorer l'erreur TLS",
"k112a7174": "Utilisation",
"k1192dd43": "Veuillez sélectionner un espace de travail",
"k11e887ab": "Voulez-vous supprimer cet élément ?",
"k11f09c87": "vues",
"k1286421": "Nom d'utilisateur",
"k134e0d97": "Actuel",
"k14c6c425": "Résultat",
"k158336d6": "Délai d'attente de la requête (s)",
"k1598e726": "visiteur actuel",
"k15ae36a6": "Navigateur",
"k16c8909f": "Nom d'affichage",
"k1777bbf2": "Manuel",
"k186365b3": "Graphique de {{monitorName}}",
"k1964b988": "Arrêter",
"k1bd89236": "exécuter le rapporteur avec",
"k1c33c293": "Paramètres",
"k1eb5b3ed": "Aperçu",
"k20edf271": "24h",
"k21077124": "Éditer",
"k2264aac": "Cette année",
"k246063be": "ID de chat",
"k2497052e": "Graphique",
"k25b3dc00": "Code JS du script",
"k264de775": "Chargement de plus...",
"k277e2626": "Disponible",
"k28059d49": "ID de l'espace de travail actuel",
"k2813d1f7": "Ce mois-ci",
"k2a6a7d8f": "HORS LIGNE",
"k2b2d40d4": "Code de test",
"k2c2712a4": "CPU",
"k2e6dbf02": "À l'email",
"k2ea8a019": "Moniteur",
"k30b5f01b": "Espaces de travail",
"k30e234ee": "Vous n'avez pas encore d'élément de tableau de bord, veuillez entrer en mode édition et ajouter votre élément.",
"k310fee": "30 derniers jours",
"k32344f64": "Effacer les données",
"k3260f019": "Déconnexion",
"k3471e956": "Répéter le nouveau mot de passe",
"k389db675": "Soumettre",
"k38c62888": "Veuillez sélectionner un moniteur",
"k3cedb797": "Sécurité",
"k3d3baf52": "temps moyen de visite",
"k3dbe79b1": "Supprimer",
"k3de768a1": "Changer le mot de passe",
"k3e757ddf": "Il n'y a pas encore de moniteur ici.",
"k43e21ee9": "Télécharger le rapporteur client",
"k44cad477": "(Actuel)",
"k46f0adde": "Carte des visiteurs",
"k47f43dbc": "Santé de {{monitorName}}",
"k4905ed7b": "AUCUN",
"k490ada32": "Ajouter un site Web",
"k49e5f1d2": "1s",
"k4ac4dd36": "Référents",
"k4de48e75": "Nombre maximum de tentatives",
"k4e08cf58": "Afficher le numéro de détail",
"k4eea9393": "Profil",
"k506a90b2": "Votre domaine de serveur, ou IP.",
"k517747e1": "24 dernières heures",
"k51bac044": "Annuler",
"k53ae02a5": "Nouveau",
"k542b527c": "Événements",
"k58f90514": "Jeton de bot",
"k593cf342": "Êtes-vous sûr de vouloir supprimer ce moniteur ?",
"k5a839f71": "Disponibilité",
"k5eb87a8b": "Démarrer",
"k5ecf04b0": "Vue",
"k6067f0ff": "Ignorer l'erreur TLS/SSL",
"k621317b5": "Nouvelle page",
"k62e19375": "Dernière mise à jour : {{date}}",
"k646a3a80": "Métriques de {{monitorName}}",
"k646c0ae2": "HDD",
"k67c5a895": "Hier",
"k683be220": "Exécuter",
"k691b7170": "Arrêté",
"k698348ff": "En attente de recevoir le pack UDP",
"k6acf5248": "Récent",
"k6b36580f": "URL Apprise",
"k6bc9e414": "Connexion",
"k6f15bcc3": "Hôte",
"k717660a5": "RAM",
"k721589c1": "Aujourd'hui",
"k74a240": "Barre de santé",
"k75581e13": "CC",
"k75bfaaa6": "Ajoutez ce code dans le script de tête de votre site web",
"k784dd132": "Test",
"k7927b824": "Êtes-vous sûr de vouloir effacer tous les nœuds hors ligne ?",
"k7ac44a6e": "Clé de session",
"k7b74a43f": "visiteurs",
"k7c95e6a5": "Êtes-vous sûr de vouloir supprimer cette page ?",
"k7cac602a": "Statut",
"k7f01b47c": "Journal d'audit",
"k7f4bcf6b": "Moniteurs",
"k8037cc6b": "Serveurs",
"k8202c669": "Visiteurs",
"k845abd5b": "Étape suivante",
"k84ce1618": "(24 heures)",
"k85344b23": "Charge",
"k85c5fd4c": "Aucun moniteur n'a été défini",
"k8746ec38": "Sélectionner un moniteur",
"k88a9bf01": "Afficher le badge",
"k88d2647b": "Site Web",
"k89056082": "(30 jours)",
"k8a44833f": "Services",
"k8bac6ae0": "6h",
"k8ef56a20": "Nombre maximal de tentatives avant que le service ne soit marqué comme étant en panne et qu'une notification soit envoyée",
"k8f8fbf6": "Partager avec...",
"k8ff3a55a": "Avertissement",
"k9022468f": "Effacer hors ligne",
"k90873752": "Ancien mot de passe",
"k90a82c67": "Créer un compte",
"k90b668e5": "24 dernières heures",
"k93374bc9": "Supprimer le site Web",
"k97ddb155": "Afficher la réponse actuelle",
"k98f433ee": "Télécharger le rapporteur de",
"k9a272ecf": "S'agit-il de vos serveurs ?",
"k9a3cc801": "Accepter les caractères",
"k9add1fac": "Trafic",
"k9be2209c": "Nom du site Web à afficher",
"k9e32beea": "en ligne",
"k9e759f8": "Liste de notifications",
"k9fa794aa": "Nom du site Web",
"ka0051b3d": "Domaine",
"ka0ddbfb": "Infos sur le site Web",
"ka2b9bc3c": "Période précédente",
"ka2fae1c6": "ID utilisateur",
"ka388d3bf": "Vous pouvez lier un moniteur qui affichera l'état de santé dans l'aperçu du site Web",
"ka40aea11": "Ou souhaitez-vous signaler l'état du serveur dans un serveur Windows ? passez à l'onglet Manuel",
"ka44150a0": "90 derniers jours",
"ka68f2242": "S'inscrire",
"ka6ee7455": "ID du site Web",
"ka71c12e1": "Les deux mots de passe ne sont pas cohérents",
"ka765ad32": "Notification",
"ka9d081ac": "Intervalle de vérification (s)",
"kaa0788e9": "Disposition enregistrée avec succès",
"kaa0ccaab": "De l'email",
"kab56db46": "Battrements de cœur",
"kacbdae07": "Réponse moyenne",
"kadef6c48": "Précédent {{label}}",
"kaf39be20": "Réseau",
"kb01f4f95": "Terminé",
"kb0e351e0": "Rafraîchi",
"kb320aac4": "Surveillé pendant {{dayNum}} jours",
"kb5673707": "7 derniers jours",
"kb659c1bc": "Expiration du cert.",
"kb8de8c50": "CCI",
"kbb58c99c": "Suppression réussie",
"kbcf67f53": "Nouveau mot de passe",
"kbd1e7dee": "Utilisation : {{usage}}ms",
"kbd425e0e": "updatedAt",
"kc00cf2c7": "Cela affichera votre résultat récent de votre moniteur",
"kc0c6a913": "Nom du nœud",
"kc1f1f6c9": "Type de notification",
"kc45a417b": "OS",
"kc4910af7": "Changer le mot de passe",
"kc4ab7848": "Effacer le nœud hors ligne",
"kc4e91854": "Êtes-vous sûr de vouloir supprimer tous les battements de cœur pour ce moniteur ?",
"kc5573507": "Ajouter",
"kc5f82d53": "Par exemple : pushdeer://pushKey",
"kc6888ac4": "Auto",
"kc6cac621": "(Aucun)",
"kc70d69ad": "Réponse",
"kc9b446d1": "Exécution terminée",
"kcacbfde1": "Créer maintenant",
"kcaf5c873": "Actions",
"kcb8fd4ce": "Aucun serveur en ligne",
"kcbc00b39": "Sélectionnez votre plage de dates",
"kcc3b034e": "Url",
"kcc50957": "Ajouter un nouveau moniteur",
"kccaa732a": "Pas de tirets consécutifs",
"kccb42483": "Mot de passe",
"kd031b383": "Vues",
"kd211e2d4": "Page des versions",
"kd37efb26": "Notifications",
"kd46ab159": "Aucun événement",
"kd7985726": "{{num}} utilisateurs",
"kd92fa3e7": "Nom de l'hôte",
"kdaa949e5": "Événements de {{monitorName}}",
"kdb61adbb": "Masquer hors ligne",
"kdc51b5db": "Sites Web",
"kde37bc27": "Retour à l'administrateur",
"kde657d5b": "Tableau de bord",
"kdeba7706": "Appareils",
"kdf5da1d2": "Aucun / STARTTLS",
"kdf97690e": "Pays",
"ke188f24b": "taux de rebond",
"ke1b5ca71": "Pages",
"ke2fe505b": "Cette semaine",
"ke3a3f2f2": "Port",
"ke46232fe": "Ajouter un serveur",
"ke5b015e9": "Vous pouvez obtenir un jeton de https://t.me/BotFather.",
"ke6797c65": "Vous pouvez obtenir votre ID de chat en envoyant un message au bot et en allant à cette URL pour voir le chat_id",
"ke9d2fef3": "Ce mois-ci",
"ke9dcaa64": "Aucune notification trouvée",
"keaf7576f": "Titre",
"ked37937b": "Métriques",
"ked7eea1a": "EN LIGNE",
"ked8814bc": "Copier",
"kedc69eb6": "Code de suivi",
"kef701e50": "Ajouter un moniteur",
"kf22813ad": "Personnalisé",
"kf3b749ef": "Prend en charge le chat direct / groupe / ID de chat de canal",
"kf55495e0": "Sauvegarder",
"kf5bbb568": "carte des visiteurs de",
"kf6bc1610": "Type de moniteur",
"kf6db9ea5": "3h",
"kf7d5dbf8": "En savoir plus",
"kf97b6f71": "Exécutez cette commande sur votre machine Linux",
"kf9877f28": "Voir les détails",
"kfc98929b": "{{num}} jours",
"kfd33c459": "Copie réussie !",
"kfdaf0bb3": "Dernière connexion : {{time}}",
"kfe11d138": "Nom",
"kfedb6cd8": "Êtes-vous sûr de vouloir supprimer tous les événements pour ce moniteur ?",
"kff849f78": "Récupération automatique"
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 450 B

View File

@ -0,0 +1,218 @@
{
"k10336b1": "これ以上読み込むものはありません",
"k10c85f27": "TLSエラーを無視",
"k112a7174": "使用法",
"k1192dd43": "ワークスペースを選択してください",
"k11e887ab": "このアイテムを削除しますか?",
"k11f09c87": "ビュー",
"k1286421": "ユーザー名",
"k134e0d97": "現在",
"k14c6c425": "結果",
"k158336d6": "リクエストタイムアウト(秒)",
"k1598e726": "現在の訪問者",
"k15ae36a6": "ブラウザ",
"k16c8909f": "表示名",
"k1777bbf2": "マニュアル",
"k186365b3": "{{monitorName}}のチャート",
"k1964b988": "停止",
"k1bd89236": "レポーターを実行する",
"k1c33c293": "設定",
"k1eb5b3ed": "概要",
"k20edf271": "24時間",
"k21077124": "編集",
"k2264aac": "今年",
"k246063be": "チャットID",
"k2497052e": "チャート",
"k25b3dc00": "スクリプトJSコード",
"k264de775": "さらに読み込む...",
"k277e2626": "利用可能",
"k28059d49": "現在のワークスペースID",
"k2813d1f7": "今月",
"k2a6a7d8f": "ダウン",
"k2b2d40d4": "テストコード",
"k2c2712a4": "CPU",
"k2e6dbf02": "メールアドレスへ",
"k2ea8a019": "モニター",
"k30b5f01b": "ワークスペース",
"k30e234ee": "ダッシュボードアイテムがまだありません。編集モードに入り、アイテムを追加してください。",
"k310fee": "過去30日間",
"k32344f64": "データクリア",
"k3260f019": "ログアウト",
"k3471e956": "新しいパスワードの再入力",
"k389db675": "送信",
"k38c62888": "モニターを選択してください",
"k3cedb797": "セキュリティ",
"k3d3baf52": "平均訪問時間",
"k3dbe79b1": "削除",
"k3de768a1": "パスワードを変更",
"k3e757ddf": "モニターがまだありません。",
"k43e21ee9": "クライアントレポーターをダウンロード",
"k44cad477": "(現在)",
"k46f0adde": "訪問者マップ",
"k47f43dbc": "{{monitorName}}の健康状態",
"k4905ed7b": "なし",
"k490ada32": "ウェブサイトを追加",
"k49e5f1d2": "1週間",
"k4ac4dd36": "リファラー",
"k4de48e75": "最大リトライ回数",
"k4e08cf58": "詳細番号を表示",
"k4eea9393": "プロファイル",
"k506a90b2": "サーバードメインまたはIP。",
"k517747e1": "過去24時間",
"k51bac044": "キャンセル",
"k53ae02a5": "新規",
"k542b527c": "イベント",
"k58f90514": "ボットトークン",
"k593cf342": "このモニターを削除してもよろしいですか?",
"k5a839f71": "アップタイム",
"k5eb87a8b": "開始",
"k5ecf04b0": "ビュー",
"k6067f0ff": "TLS/SSLエラーを無視",
"k621317b5": "新しいページ",
"k62e19375": "最終更新:{{date}}",
"k646a3a80": "{{monitorName}}のメトリック",
"k646c0ae2": "HDD",
"k67c5a895": "昨日",
"k683be220": "実行",
"k691b7170": "停止済み",
"k698348ff": "UDPパックの受信待ち",
"k6acf5248": "最近",
"k6b36580f": "Apprise URL",
"k6bc9e414": "ログイン",
"k6f15bcc3": "ホスト",
"k717660a5": "RAM",
"k721589c1": "今日",
"k74a240": "ヘルスバー",
"k75581e13": "CC",
"k75bfaaa6": "このコードをウェブサイトのヘッドスクリプトに追加してください",
"k784dd132": "テスト",
"k7927b824": "すべてのオフラインノードをクリアしてもよろしいですか?",
"k7ac44a6e": "セッションキー",
"k7b74a43f": "訪問者",
"k7c95e6a5": "このページを削除してもよろしいですか?",
"k7cac602a": "ステータス",
"k7f01b47c": "監査ログ",
"k7f4bcf6b": "モニター",
"k8037cc6b": "サーバー",
"k8202c669": "訪問者",
"k845abd5b": "次のステップ",
"k84ce1618": "24時間",
"k85344b23": "ロード",
"k85c5fd4c": "まだモニターが設定されていません",
"k8746ec38": "モニターを選択",
"k88a9bf01": "バッジを表示",
"k88d2647b": "ウェブサイト",
"k89056082": "30日間",
"k8a44833f": "サービス",
"k8bac6ae0": "6時間",
"k8ef56a20": "サービスがダウンとマークされ、通知が送信される前の最大リトライ回数",
"k8f8fbf6": "共有...",
"k8ff3a55a": "警告",
"k9022468f": "オフラインをクリア",
"k90873752": "古いパスワード",
"k90a82c67": "アカウントを登録",
"k90b668e5": "過去24時間",
"k93374bc9": "ウェブサイトを削除",
"k97ddb155": "現在の応答を表示",
"k98f433ee": "からレポーターをダウンロード",
"k9a272ecf": "これはあなたのサーバーですか?",
"k9a3cc801": "文字を受け入れる",
"k9add1fac": "トラフィック",
"k9be2209c": "表示するウェブサイト名",
"k9e32beea": "オンライン",
"k9e759f8": "通知リスト",
"k9fa794aa": "ウェブサイト名",
"ka0051b3d": "ドメイン",
"ka0ddbfb": "ウェブサイト情報",
"ka2b9bc3c": "前の期間",
"ka2fae1c6": "ユーザーID",
"ka388d3bf": "ウェブサイトの概要に表示されるヘルスステータスを表示するモニターをバインドできます",
"ka40aea11": "Windowsサーバーでサーバーステータスを報告しますかマニュアルタブに切り替えてください",
"ka44150a0": "過去90日間",
"ka68f2242": "登録",
"ka6ee7455": "ウェブサイトID",
"ka71c12e1": "2つのパスワードが一致しません",
"ka765ad32": "通知",
"ka9d081ac": "チェック間隔(秒)",
"kaa0788e9": "レイアウトが正常に保存されました",
"kaa0ccaab": "送信者メール",
"kab56db46": "ハートビート",
"kacbdae07": "平均応答",
"kadef6c48": "前の{{label}}",
"kaf39be20": "ネットワーク",
"kb01f4f95": "完了",
"kb0e351e0": "更新されました",
"kb320aac4": "{{dayNum}}日間監視",
"kb5673707": "過去7日間",
"kb659c1bc": "証明書の有効期限",
"kb8de8c50": "BCC",
"kbb58c99c": "削除に成功しました",
"kbcf67f53": "新しいパスワード",
"kbd1e7dee": "使用量:{{usage}}ms",
"kbd425e0e": "更新日",
"kc00cf2c7": "これはあなたのモニターの最近の結果を表示します",
"kc0c6a913": "ノード名",
"kc1f1f6c9": "通知タイプ",
"kc45a417b": "OS",
"kc4910af7": "パスワードを変更する",
"kc4ab7848": "オフラインノードをクリア",
"kc4e91854": "このモニターのすべてのハートビートを削除してもよろしいですか?",
"kc5573507": "追加",
"kc5f82d53": "例pushdeer://pushKey",
"kc6888ac4": "自動",
"kc6cac621": "(なし)",
"kc70d69ad": "応答",
"kc9b446d1": "実行完了",
"kcacbfde1": "今すぐ作成",
"kcaf5c873": "アクション",
"kcb8fd4ce": "オンラインのサーバーはありません",
"kcbc00b39": "日付範囲を選択",
"kcc3b034e": "Url",
"kcc50957": "新しいモニターを追加",
"kccaa732a": "連続ダッシュなし",
"kccb42483": "パスワード",
"kd031b383": "ビュー",
"kd211e2d4": "リリースページ",
"kd37efb26": "通知",
"kd46ab159": "イベントなし",
"kd7985726": "{{num}}人のユーザー",
"kd92fa3e7": "ホスト名",
"kdaa949e5": "{{monitorName}}のイベント",
"kdb61adbb": "オフラインを隠す",
"kdc51b5db": "ウェブサイト",
"kde37bc27": "管理者に戻る",
"kde657d5b": "ダッシュボード",
"kdeba7706": "デバイス",
"kdf5da1d2": "なし/STARTTLS",
"kdf97690e": "国",
"ke188f24b": "直帰率",
"ke1b5ca71": "ページ",
"ke2fe505b": "今週",
"ke3a3f2f2": "ポート",
"ke46232fe": "サーバーを追加",
"ke5b015e9": "https://t.me/BotFather でトークンを取得できます。",
"ke6797c65": "ボットにメッセージを送り、このURLにアクセスしてchat_idを表示することで、チャットIDを取得できます",
"ke9d2fef3": "今月",
"ke9dcaa64": "通知は見つかりませんでした",
"keaf7576f": "タイトル",
"ked37937b": "メトリクス",
"ked7eea1a": "アップ",
"ked8814bc": "コピー",
"kedc69eb6": "トラッキングコード",
"kef701e50": "モニターを追加",
"kf22813ad": "カスタム",
"kf3b749ef": "ダイレクトチャット/グループ/チャネルのチャットIDをサポート",
"kf55495e0": "保存",
"kf5bbb568": "の訪問者マップ",
"kf6bc1610": "モニタータイプ",
"kf6db9ea5": "3時間",
"kf7d5dbf8": "もっと読む",
"kf97b6f71": "Linuxマシンでこのコマンドを実行してください",
"kf9877f28": "詳細を見る",
"kfc98929b": "{{num}}日",
"kfd33c459": "コピーに成功しました!",
"kfdaf0bb3": "最後のオンライン:{{time}}",
"kfe11d138": "名前",
"kfedb6cd8": "このモニターのすべてのイベントを削除してもよろしいですか?",
"kff849f78": "自動取得"
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 268 B

View File

@ -0,0 +1,218 @@
{
"k10336b1": "Больше ничего загружать не нужно",
"k10c85f27": "Игнорировать ошибку TLS",
"k112a7174": "Использование",
"k1192dd43": "Пожалуйста, выберите рабочую область",
"k11e887ab": "Удалить этот элемент?",
"k11f09c87": "просмотры",
"k1286421": "Имя пользователя",
"k134e0d97": "Текущий",
"k14c6c425": "Результат",
"k158336d6": "Таймаут запроса (с)",
"k1598e726": "текущий посетитель",
"k15ae36a6": "Браузер",
"k16c8909f": "Отображаемое имя",
"k1777bbf2": "Вручную",
"k186365b3": "График {{monitorName}}",
"k1964b988": "Остановить",
"k1bd89236": "запустить репортер с",
"k1c33c293": "Настройки",
"k1eb5b3ed": "Обзор",
"k20edf271": "24ч",
"k21077124": "Редактировать",
"k2264aac": "Этот год",
"k246063be": "ID чата",
"k2497052e": "График",
"k25b3dc00": "JS код скрипта",
"k264de775": "Загружается ещё...",
"k277e2626": "Доступно",
"k28059d49": "ID текущей рабочей области",
"k2813d1f7": "Этот месяц",
"k2a6a7d8f": "НИЖЕ",
"k2b2d40d4": "Тестовый код",
"k2c2712a4": "ЦПУ",
"k2e6dbf02": "На Email",
"k2ea8a019": "Монитор",
"k30b5f01b": "Рабочие области",
"k30e234ee": "У вас еще нет элементов на панели инструментов, пожалуйста, войдите в режим редактирования и добавьте свой элемент.",
"k310fee": "Последние 30 дней",
"k32344f64": "Очистить данные",
"k3260f019": "Выйти",
"k3471e956": "Повтор нового пароля",
"k389db675": "Отправить",
"k38c62888": "Пожалуйста, выберите монитор",
"k3cedb797": "Безопасность",
"k3d3baf52": "среднее время посещения",
"k3dbe79b1": "Удалить",
"k3de768a1": "Сменить пароль",
"k3e757ddf": "Здесь пока нет мониторов.",
"k43e21ee9": "Скачать клиентский отчет",
"k44cad477": "(Текущий)",
"k46f0adde": "Карта посетителей",
"k47f43dbc": "Здоровье {{monitorName}}",
"k4905ed7b": "НИКАКОЙ",
"k490ada32": "Добавить веб-сайт",
"k49e5f1d2": "1н",
"k4ac4dd36": "Рефереры",
"k4de48e75": "Макс. попыток",
"k4e08cf58": "Показать подробное количество",
"k4eea9393": "Профиль",
"k506a90b2": "Ваш домен сервера или ip.",
"k517747e1": "Последние 24 часа",
"k51bac044": "Отмена",
"k53ae02a5": "Новый",
"k542b527c": "События",
"k58f90514": "Токен бота",
"k593cf342": "Вы уверены, что хотите удалить этот монитор?",
"k5a839f71": "Время работы",
"k5eb87a8b": "Старт",
"k5ecf04b0": "Просмотр",
"k6067f0ff": "Игнорировать ошибку TLS/SSL",
"k621317b5": "Новая страница",
"k62e19375": "Последнее обновление: {{date}}",
"k646a3a80": "Метрики {{monitorName}}",
"k646c0ae2": "HDD",
"k67c5a895": "Вчера",
"k683be220": "Запустить",
"k691b7170": "Остановлено",
"k698348ff": "Ожидание получения UDP пакета",
"k6acf5248": "Недавние",
"k6b36580f": "URL Apprise",
"k6bc9e414": "Вход",
"k6f15bcc3": "Хост",
"k717660a5": "ОЗУ",
"k721589c1": "Сегодня",
"k74a240": "Полоса здоровья",
"k75581e13": "Копия",
"k75bfaaa6": "Добавьте этот код в скрипт заголовка вашего веб-сайта",
"k784dd132": "Тест",
"k7927b824": "Вы уверены, что хотите очистить все офлайн узлы?",
"k7ac44a6e": "Ключ сессии",
"k7b74a43f": "посетители",
"k7c95e6a5": "Вы уверены, что хотите удалить эту страницу?",
"k7cac602a": "Статус",
"k7f01b47c": "Журнал аудита",
"k7f4bcf6b": "Мониторы",
"k8037cc6b": "Серверы",
"k8202c669": "Посетители",
"k845abd5b": "Следующий шаг",
"k84ce1618": "(24 часа)",
"k85344b23": "Нагрузка",
"k85c5fd4c": "Мониторы еще не настроены",
"k8746ec38": "Выбрать монитор",
"k88a9bf01": "Показать значок",
"k88d2647b": "Веб-сайт",
"k89056082": "(30 дней)",
"k8a44833f": "Сервисы",
"k8bac6ae0": "6ч",
"k8ef56a20": "Максимальное количество попыток перед тем, как сервис будет помечен как недоступный и будет отправлено уведомление",
"k8f8fbf6": "Поделиться с...",
"k8ff3a55a": "Предупреждение",
"k9022468f": "Очистить офлайн",
"k90873752": "Старый пароль",
"k90a82c67": "Зарегистрировать аккаунт",
"k90b668e5": "Последние 24 часа",
"k93374bc9": "Удалить веб-сайт",
"k97ddb155": "Показать текущий ответ",
"k98f433ee": "Скачать репортер с",
"k9a272ecf": "Это ваши серверы?",
"k9a3cc801": "Принимаемые символы",
"k9add1fac": "Трафик",
"k9be2209c": "Отображаемое имя веб-сайта",
"k9e32beea": "онлайн",
"k9e759f8": "Список уведомлений",
"k9fa794aa": "Название веб-сайта",
"ka0051b3d": "Домен",
"ka0ddbfb": "Информация о веб-сайте",
"ka2b9bc3c": "Предыдущий период",
"ka2fae1c6": "ID пользователя",
"ka388d3bf": "Вы можете привязать монитор, который будет отображать статус здоровья в обзоре веб-сайта",
"ka40aea11": "Или вы хотите сообщать о состоянии сервера в Windows сервере? переключитесь на вкладку Ручной ввод",
"ka44150a0": "Последние 90 дней",
"ka68f2242": "Регистрация",
"ka6ee7455": "ID веб-сайта",
"ka71c12e1": "Два пароля не совпадают",
"ka765ad32": "Уведомления",
"ka9d081ac": "Интервал проверки (с)",
"kaa0788e9": "Макет успешно сохранен",
"kaa0ccaab": "Email отправителя",
"kab56db46": "Сердцебиение",
"kacbdae07": "Среднее время ответа",
"kadef6c48": "Предыдущий {{label}}",
"kaf39be20": "Сеть",
"kb01f4f95": "Готово",
"kb0e351e0": "Обновлено",
"kb320aac4": "Мониторинг в течение {{dayNum}} дней",
"kb5673707": "Последние 7 дней",
"kb659c1bc": "Истечение серт.",
"kb8de8c50": "Скрытая копия",
"kbb58c99c": "Успешно удалено",
"kbcf67f53": "Новый пароль",
"kbd1e7dee": "Использование: {{usage}}мс",
"kbd425e0e": "Обновлено в",
"kc00cf2c7": "Здесь будет показан ваш последний результат мониторинга",
"kc0c6a913": "Имя узла",
"kc1f1f6c9": "Тип уведомления",
"kc45a417b": "ОС",
"kc4910af7": "Смена пароля",
"kc4ab7848": "Очистить офлайн узлы",
"kc4e91854": "Вы уверены, что хотите удалить все сердцебиения для этого монитора?",
"kc5573507": "Добавить",
"kc5f82d53": "Например: pushdeer://pushKey",
"kc6888ac4": "Авто",
"kc6cac621": "(Нет)",
"kc70d69ad": "Ответ",
"kc9b446d1": "Запуск завершен",
"kcacbfde1": "Создать сейчас",
"kcaf5c873": "Действия",
"kcb8fd4ce": "Онлайн серверов нет",
"kcbc00b39": "Выберите диапазон дат",
"kcc3b034e": "Url",
"kcc50957": "Добавить новый монитор",
"kccaa732a": "Без последовательных тире",
"kccb42483": "Пароль",
"kd031b383": "Просмотры",
"kd211e2d4": "Страница релизов",
"kd37efb26": "Уведомления",
"kd46ab159": "Событий нет",
"kd7985726": "{{num}} пользователей",
"kd92fa3e7": "Имя хоста",
"kdaa949e5": "События {{monitorName}}",
"kdb61adbb": "Скрыть офлайн",
"kdc51b5db": "Веб-сайты",
"kde37bc27": "Вернуться к администратору",
"kde657d5b": "Панель управления",
"kdeba7706": "Устройства",
"kdf5da1d2": "Нет / STARTTLS",
"kdf97690e": "Страны",
"ke188f24b": "коэффициент отказов",
"ke1b5ca71": "Страницы",
"ke2fe505b": "На этой неделе",
"ke3a3f2f2": "Порт",
"ke46232fe": "Добавить сервер",
"ke5b015e9": "Вы можете получить токен на https://t.me/BotFather.",
"ke6797c65": "Вы можете получить свой ID чата, отправив сообщение боту и перейдя по этому URL, чтобы просмотреть chat_id",
"ke9d2fef3": "Этот месяц",
"ke9dcaa64": "Уведомлений не найдено",
"keaf7576f": "Заголовок",
"ked37937b": "Метрики",
"ked7eea1a": "ВВЕРХ",
"ked8814bc": "Копировать",
"kedc69eb6": "Код отслеживания",
"kef701e50": "Добавить монитор",
"kf22813ad": "Пользовательский",
"kf3b749ef": "Поддержка прямого чата / группы / ID чата канала",
"kf55495e0": "Сохранить",
"kf5bbb568": "карта посетителей",
"kf6bc1610": "Тип монитора",
"kf6db9ea5": "3ч",
"kf7d5dbf8": "Читать далее",
"kf97b6f71": "Запустите эту команду на вашем Linux-машине",
"kf9877f28": "Посмотреть детали",
"kfc98929b": "{{num}} дней",
"kfd33c459": "Копирование успешно!",
"kfdaf0bb3": "Последний онлайн: {{time}}",
"kfe11d138": "Имя",
"kfedb6cd8": "Вы уверены, что хотите удалить все события для этого монитора?",
"kff849f78": "Автоизвлечение"
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@ -0,0 +1,218 @@
{
"k10336b1": "没有更多内容加载",
"k10c85f27": "忽略 TLS 错误",
"k112a7174": "使用",
"k1192dd43": "请选择工作区",
"k11e887ab": "确定要删除这个项目吗?",
"k11f09c87": "视图",
"k1286421": "用户名",
"k134e0d97": "当前",
"k14c6c425": "结果",
"k158336d6": "请求超时(秒)",
"k1598e726": "当前访问者",
"k15ae36a6": "浏览器",
"k16c8909f": "显示名称",
"k1777bbf2": "手册",
"k186365b3": "{{monitorName}}的图表",
"k1964b988": "停止",
"k1bd89236": "运行报告器",
"k1c33c293": "设置",
"k1eb5b3ed": "概览",
"k20edf271": "24小时",
"k21077124": "编辑",
"k2264aac": "今年",
"k246063be": "聊天 ID",
"k2497052e": "图表",
"k25b3dc00": "脚本 JS 代码",
"k264de775": "加载更多...",
"k277e2626": "可用",
"k28059d49": "当前工作区 ID",
"k2813d1f7": "本月",
"k2a6a7d8f": "下线",
"k2b2d40d4": "测试代码",
"k2c2712a4": "CPU",
"k2e6dbf02": "发邮件到",
"k2ea8a019": "监控器",
"k30b5f01b": "工作区",
"k30e234ee": "您还没有仪表板项目,请进入编辑模式并添加您的项目。",
"k310fee": "最近30天",
"k32344f64": "清除数据",
"k3260f019": "登出",
"k3471e956": "重复新密码",
"k389db675": "提交",
"k38c62888": "请选择监控器",
"k3cedb797": "安全",
"k3d3baf52": "平均访问时间",
"k3dbe79b1": "删除",
"k3de768a1": "更改密码",
"k3e757ddf": "这里还没有监控器。",
"k43e21ee9": "下载客户端报告器",
"k44cad477": "(当前)",
"k46f0adde": "访问者地图",
"k47f43dbc": "{{monitorName}}的健康状况",
"k4905ed7b": "无",
"k490ada32": "添加网站",
"k49e5f1d2": "1周",
"k4ac4dd36": "引荐人",
"k4de48e75": "最大重试次数",
"k4e08cf58": "显示详细数字",
"k4eea9393": "个人资料",
"k506a90b2": "您的服务器域名或IP。",
"k517747e1": "最近24小时",
"k51bac044": "取消",
"k53ae02a5": "新建",
"k542b527c": "事件",
"k58f90514": "机器人令牌",
"k593cf342": "您确定要删除这个监控器吗?",
"k5a839f71": "正常运行时间",
"k5eb87a8b": "开始",
"k5ecf04b0": "查看",
"k6067f0ff": "忽略 TLS/SSL 错误",
"k621317b5": "新页面",
"k62e19375": "最后更新时间:{{date}}",
"k646a3a80": "{{monitorName}}的指标",
"k646c0ae2": "HDD",
"k67c5a895": "昨天",
"k683be220": "运行",
"k691b7170": "已停止",
"k698348ff": "等待接收 UDP 包",
"k6acf5248": "最近",
"k6b36580f": "Apprise URL",
"k6bc9e414": "登录",
"k6f15bcc3": "主机",
"k717660a5": "RAM",
"k721589c1": "今天",
"k74a240": "健康栏",
"k75581e13": "CC",
"k75bfaaa6": "将此代码添加到您的网站头部脚本中",
"k784dd132": "测试",
"k7927b824": "您确定要清除所有离线节点吗?",
"k7ac44a6e": "会话密钥",
"k7b74a43f": "访问者",
"k7c95e6a5": "您确定要删除此页面吗?",
"k7cac602a": "状态",
"k7f01b47c": "审计日志",
"k7f4bcf6b": "监控器",
"k8037cc6b": "服务器",
"k8202c669": "访问者",
"k845abd5b": "下一步",
"k84ce1618": "24小时",
"k85344b23": "负载",
"k85c5fd4c": "还没有设置任何监控器",
"k8746ec38": "选择监控器",
"k88a9bf01": "显示徽章",
"k88d2647b": "网站",
"k89056082": "30天",
"k8a44833f": "服务",
"k8bac6ae0": "6小时",
"k8ef56a20": "服务被标记为下线并发送通知前的最大重试次数",
"k8f8fbf6": "分享给...",
"k8ff3a55a": "警告",
"k9022468f": "清除离线",
"k90873752": "旧密码",
"k90a82c67": "注册账户",
"k90b668e5": "最近24小时",
"k93374bc9": "删除网站",
"k97ddb155": "显示当前响应",
"k98f433ee": "从这里下载报告器",
"k9a272ecf": "这是您的服务器吗?",
"k9a3cc801": "接受字符",
"k9add1fac": "流量",
"k9be2209c": "要显示的网站名称",
"k9e32beea": "在线",
"k9e759f8": "通知列表",
"k9fa794aa": "网站名称",
"ka0051b3d": "域名",
"ka0ddbfb": "网站信息",
"ka2b9bc3c": "上一个周期",
"ka2fae1c6": "用户ID",
"ka388d3bf": "您可以绑定一个监控器,它将在网站概览中显示健康状态",
"ka40aea11": "或者您想在Windows服务器中报告服务器状态切换到手动选项卡",
"ka44150a0": "最近90天",
"ka68f2242": "注册",
"ka6ee7455": "网站ID",
"ka71c12e1": "两次密码不一致",
"ka765ad32": "通知",
"ka9d081ac": "检查间隔(秒)",
"kaa0788e9": "布局保存成功",
"kaa0ccaab": "发件人邮箱",
"kab56db46": "心跳",
"kacbdae07": "平均响应",
"kadef6c48": "上一个{{label}}",
"kaf39be20": "网络",
"kb01f4f95": "完成",
"kb0e351e0": "已刷新",
"kb320aac4": "已监控{{dayNum}}天",
"kb5673707": "最近7天",
"kb659c1bc": "证书到期",
"kb8de8c50": "密送",
"kbb58c99c": "删除成功",
"kbcf67f53": "新密码",
"kbd1e7dee": "使用量:{{usage}}毫秒",
"kbd425e0e": "更新于",
"kc00cf2c7": "这将显示您的监控器的最近结果",
"kc0c6a913": "节点名称",
"kc1f1f6c9": "通知类型",
"kc45a417b": "操作系统",
"kc4910af7": "更改密码",
"kc4ab7848": "清除离线节点",
"kc4e91854": "您确定要删除此监控器的所有心跳吗?",
"kc5573507": "添加",
"kc5f82d53": "例如pushdeer://pushKey",
"kc6888ac4": "自动",
"kc6cac621": "(无)",
"kc70d69ad": "响应",
"kc9b446d1": "运行完成",
"kcacbfde1": "立即创建",
"kcaf5c873": "操作",
"kcb8fd4ce": "没有在线服务器",
"kcbc00b39": "选择您的日期范围",
"kcc3b034e": "链接",
"kcc50957": "添加新的监控器",
"kccaa732a": "无连续破折号",
"kccb42483": "密码",
"kd031b383": "视图",
"kd211e2d4": "发布页面",
"kd37efb26": "通知",
"kd46ab159": "没有事件",
"kd7985726": "{{num}}个用户",
"kd92fa3e7": "主机名",
"kdaa949e5": "{{monitorName}}的事件",
"kdb61adbb": "隐藏离线",
"kdc51b5db": "网站",
"kde37bc27": "返回管理员",
"kde657d5b": "仪表板",
"kdeba7706": "设备",
"kdf5da1d2": "无/STARTTLS",
"kdf97690e": "国家",
"ke188f24b": "跳出率",
"ke1b5ca71": "页面",
"ke2fe505b": "本周",
"ke3a3f2f2": "端口",
"ke46232fe": "添加服务器",
"ke5b015e9": "您可以从 https://t.me/BotFather 获取令牌。",
"ke6797c65": "您可以通过向机器人发送消息并访问此URL来查看chat_id以获取您的聊天ID",
"ke9d2fef3": "本月",
"ke9dcaa64": "没有找到任何通知",
"keaf7576f": "标题",
"ked37937b": "指标",
"ked7eea1a": "上线",
"ked8814bc": "复制",
"kedc69eb6": "追踪代码",
"kef701e50": "添加监控器",
"kf22813ad": "自定义",
"kf3b749ef": "支持直接聊天/群组/频道的聊天ID",
"kf55495e0": "保存",
"kf5bbb568": "的访问者地图",
"kf6bc1610": "监控器类型",
"kf6db9ea5": "3小时",
"kf7d5dbf8": "阅读更多",
"kf97b6f71": "在您的Linux机器上运行此命令",
"kf9877f28": "查看详情",
"kfc98929b": "{{num}}天",
"kfd33c459": "复制成功!",
"kfdaf0bb3": "最后在线时间:{{time}}",
"kfe11d138": "名称",
"kfedb6cd8": "您确定要删除此监控器的所有事件吗?",
"kff849f78": "自动获取"
}