commit
9b92583893
|
@ -23,6 +23,7 @@
|
|||
.DS_Store
|
||||
.idea
|
||||
*.iml
|
||||
*.log
|
||||
.vscode/*
|
||||
|
||||
# debug
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 512"><path d="M296 160H180.6l42.6-129.8C227.2 15 215.7 0 200 0H56C44 0 33.8 8.9 32.2 20.8l-32 240C-1.7 275.2 9.5 288 24 288h118.7L96.6 482.5c-3.6 15.2 8 29.5 23.3 29.5 8.4 0 16.4-4.4 20.8-12l176-304c9.3-15.9-2.2-36-20.7-36z"/></svg>
|
After Width: | Height: | Size: 289 B |
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512"><path d="M288 144a110.94 110.94 0 0 0-31.24 5 55.4 55.4 0 0 1 7.24 27 56 56 0 0 1-56 56 55.4 55.4 0 0 1-27-7.24A111.71 111.71 0 1 0 288 144zm284.52 97.4C518.29 135.59 410.93 64 288 64S57.68 135.64 3.48 241.41a32.35 32.35 0 0 0 0 29.19C57.71 376.41 165.07 448 288 448s230.32-71.64 284.52-177.41a32.35 32.35 0 0 0 0-29.19zM288 400c-98.65 0-189.09-55-237.93-144C98.91 167 189.34 112 288 112s189.09 55 237.93 144C477.1 345 386.66 400 288 400z"/></svg>
|
After Width: | Height: | Size: 509 B |
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><path d="M224 256c70.7 0 128-57.3 128-128S294.7 0 224 0 96 57.3 96 128s57.3 128 128 128zm89.6 32h-16.7c-22.2 10.2-46.9 16-72.9 16s-50.6-5.8-72.9-16h-16.7C60.2 288 0 348.2 0 422.4V464c0 26.5 21.5 48 48 48h352c26.5 0 48-21.5 48-48v-41.6c0-74.2-60.2-134.4-134.4-134.4z"/></svg>
|
After Width: | Height: | Size: 336 B |
|
@ -3,7 +3,7 @@ import Button from './Button';
|
|||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
const defaultText = (
|
||||
<FormattedMessage id="button.copy-to-clipboard" defaultMessage="Copy to clipboard" />
|
||||
<FormattedMessage id="label.copy-to-clipboard" defaultMessage="Copy to clipboard" />
|
||||
);
|
||||
|
||||
export default function CopyButton({ element, ...props }) {
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import styles from './Dot.module.css';
|
||||
|
||||
export default function Dot({ color, size, className }) {
|
||||
return (
|
||||
<div className={styles.wrapper}>
|
||||
<div
|
||||
style={{ background: color }}
|
||||
className={classNames(styles.dot, className, {
|
||||
[styles.small]: size === 'small',
|
||||
[styles.large]: size === 'large',
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
.wrapper {
|
||||
background: var(--gray50);
|
||||
margin-right: 10px;
|
||||
border-radius: 100%;
|
||||
}
|
||||
|
||||
.dot {
|
||||
background: var(--green400);
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 100%;
|
||||
}
|
||||
|
||||
.dot.small {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
.dot.large {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
|
@ -6,7 +6,7 @@ import styles from './EmptyPlaceholder.module.css';
|
|||
export default function EmptyPlaceholder({ msg, children }) {
|
||||
return (
|
||||
<div className={styles.placeholder}>
|
||||
<Icon icon={<Logo />} size="xlarge" />
|
||||
<Icon className={styles.icon} icon={<Logo />} size="xlarge" />
|
||||
<h2>{msg}</h2>
|
||||
{children}
|
||||
</div>
|
||||
|
|
|
@ -5,3 +5,7 @@
|
|||
align-items: center;
|
||||
min-height: 600px;
|
||||
}
|
||||
|
||||
.icon {
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
import React from 'react';
|
||||
import styles from './Favicon.module.css';
|
||||
|
||||
function getHostName(url) {
|
||||
const match = url.match(/^(?:https?:\/\/)?(?:[^@\n]+@)?(?:www\.)?([^:/\n?=]+)/im);
|
||||
return match && match.length > 1 ? match[1] : null;
|
||||
}
|
||||
|
||||
export default function Favicon({ domain, ...props }) {
|
||||
const hostName = domain ? getHostName(domain) : null;
|
||||
|
||||
return hostName ? (
|
||||
<img
|
||||
className={styles.favicon}
|
||||
src={`https://icons.duckduckgo.com/ip3/${hostName}.ico`}
|
||||
height="16"
|
||||
alt=""
|
||||
{...props}
|
||||
/>
|
||||
) : null;
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
.favicon {
|
||||
margin-right: 8px;
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
import React from 'react';
|
||||
import ButtonLayout from 'components/layout/ButtonLayout';
|
||||
import ButtonGroup from './ButtonGroup';
|
||||
|
||||
export default function FilterButtons({ buttons, selected, onClick }) {
|
||||
return (
|
||||
<ButtonLayout>
|
||||
<ButtonGroup size="xsmall" items={buttons} selectedItem={selected} onClick={onClick} />
|
||||
</ButtonLayout>
|
||||
);
|
||||
}
|
|
@ -1,5 +1,6 @@
|
|||
.container {
|
||||
color: var(--gray500);
|
||||
font-size: var(--font-size-normal);
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
|
|
|
@ -12,7 +12,7 @@ export default function RefreshButton({ websiteId }) {
|
|||
const dispatch = useDispatch();
|
||||
const [dateRange] = useDateRange(websiteId);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const completed = useSelector(state => state.queries[`/api/website/${websiteId}/metrics`]);
|
||||
const completed = useSelector(state => state.queries[`/api/website/${websiteId}/stats`]);
|
||||
|
||||
function handleClick() {
|
||||
if (dateRange) {
|
||||
|
@ -28,7 +28,7 @@ export default function RefreshButton({ websiteId }) {
|
|||
return (
|
||||
<Button
|
||||
icon={loading ? <Dots /> : <Refresh />}
|
||||
tooltip={<FormattedMessage id="button.refresh" defaultMessage="Refresh" />}
|
||||
tooltip={<FormattedMessage id="label.refresh" defaultMessage="Refresh" />}
|
||||
tooltipId="button-refresh"
|
||||
size="small"
|
||||
onClick={handleClick}
|
||||
|
|
|
@ -3,40 +3,58 @@ import classNames from 'classnames';
|
|||
import NoData from 'components/common/NoData';
|
||||
import styles from './Table.module.css';
|
||||
|
||||
export default function Table({ columns, rows, empty }) {
|
||||
export default function Table({
|
||||
columns,
|
||||
rows,
|
||||
empty,
|
||||
className,
|
||||
bodyClassName,
|
||||
rowKey,
|
||||
showHeader = true,
|
||||
children,
|
||||
}) {
|
||||
if (empty && rows.length === 0) {
|
||||
return empty;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.table}>
|
||||
<div className={classNames(styles.header, 'row')}>
|
||||
{columns.map(({ key, label, className, style, header }) => (
|
||||
<div
|
||||
key={key}
|
||||
className={classNames(styles.head, className, header?.className)}
|
||||
style={{ ...style, ...header?.style }}
|
||||
>
|
||||
{label}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className={styles.body}>
|
||||
<div className={classNames(styles.table, className)}>
|
||||
{showHeader && (
|
||||
<div className={classNames(styles.header, 'row')}>
|
||||
{columns.map(({ key, label, className, style, header }) => (
|
||||
<div
|
||||
key={key}
|
||||
className={classNames(styles.head, className, header?.className)}
|
||||
style={{ ...style, ...header?.style }}
|
||||
>
|
||||
{label}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<div className={classNames(styles.body, bodyClassName)}>
|
||||
{rows.length === 0 && <NoData />}
|
||||
{rows.map((row, rowIndex) => (
|
||||
<div className={classNames(styles.row, 'row')} key={rowIndex}>
|
||||
{columns.map(({ key, render, className, style, cell }) => (
|
||||
<div
|
||||
key={`${rowIndex}${key}`}
|
||||
className={classNames(styles.cell, className, cell?.className)}
|
||||
style={{ ...style, ...cell?.style }}
|
||||
>
|
||||
{render ? render(row) : row[key]}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
{!children &&
|
||||
rows.map((row, index) => {
|
||||
const id = rowKey ? rowKey(row) : index;
|
||||
return <TableRow key={id} columns={columns} row={row} />;
|
||||
})}
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export const TableRow = ({ columns, row }) => (
|
||||
<div className={classNames(styles.row, 'row')}>
|
||||
{columns.map(({ key, render, className, style, cell }, index) => (
|
||||
<div
|
||||
key={`${key}-${index}`}
|
||||
className={classNames(styles.cell, className, cell?.className)}
|
||||
style={{ ...style, ...cell?.style }}
|
||||
>
|
||||
{render ? render(row) : row[key]}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import styles from './Tag.module.css';
|
||||
|
||||
export default function Tag({ className, children }) {
|
||||
return <span className={classNames(styles.tag, className)}>{children}</span>;
|
||||
}
|
|
@ -1,5 +1,4 @@
|
|||
.type {
|
||||
font-size: var(--font-size-small);
|
||||
.tag {
|
||||
padding: 2px 4px;
|
||||
border: 1px solid var(--gray300);
|
||||
border-radius: 4px;
|
|
@ -36,10 +36,10 @@ export default function UpdateNotice() {
|
|||
</div>
|
||||
<ButtonLayout>
|
||||
<Button size="xsmall" variant="action" onClick={handleViewClick}>
|
||||
<FormattedMessage id="button.view-details" defaultMessage="View details" />
|
||||
<FormattedMessage id="label.view-details" defaultMessage="View details" />
|
||||
</Button>
|
||||
<Button size="xsmall" onClick={handleDismissClick}>
|
||||
<FormattedMessage id="button.dismiss" defaultMessage="Dismiss" />
|
||||
<FormattedMessage id="label.dismiss" defaultMessage="Dismiss" />
|
||||
</Button>
|
||||
</ButtonLayout>
|
||||
</div>
|
||||
|
|
|
@ -58,22 +58,26 @@ export default function AccountEditForm({ values, onSave, onClose }) {
|
|||
<label htmlFor="username">
|
||||
<FormattedMessage id="label.username" defaultMessage="Username" />
|
||||
</label>
|
||||
<Field name="username" type="text" />
|
||||
<FormError name="username" />
|
||||
<div>
|
||||
<Field name="username" type="text" />
|
||||
<FormError name="username" />
|
||||
</div>
|
||||
</FormRow>
|
||||
<FormRow>
|
||||
<label htmlFor="password">
|
||||
<FormattedMessage id="label.password" defaultMessage="Password" />
|
||||
</label>
|
||||
<Field name="password" type="password" />
|
||||
<FormError name="password" />
|
||||
<div>
|
||||
<Field name="password" type="password" />
|
||||
<FormError name="password" />
|
||||
</div>
|
||||
</FormRow>
|
||||
<FormButtons>
|
||||
<Button type="submit" variant="action">
|
||||
<FormattedMessage id="button.save" defaultMessage="Save" />
|
||||
<FormattedMessage id="label.save" defaultMessage="Save" />
|
||||
</Button>
|
||||
<Button onClick={onClose}>
|
||||
<FormattedMessage id="button.cancel" defaultMessage="Cancel" />
|
||||
<FormattedMessage id="label.cancel" defaultMessage="Cancel" />
|
||||
</Button>
|
||||
</FormButtons>
|
||||
<FormMessage>{message}</FormMessage>
|
||||
|
|
|
@ -66,29 +66,35 @@ export default function ChangePasswordForm({ values, onSave, onClose }) {
|
|||
<label htmlFor="current_password">
|
||||
<FormattedMessage id="label.current-password" defaultMessage="Current password" />
|
||||
</label>
|
||||
<Field name="current_password" type="password" />
|
||||
<FormError name="current_password" />
|
||||
<div>
|
||||
<Field name="current_password" type="password" />
|
||||
<FormError name="current_password" />
|
||||
</div>
|
||||
</FormRow>
|
||||
<FormRow>
|
||||
<label htmlFor="new_password">
|
||||
<FormattedMessage id="label.new-password" defaultMessage="New password" />
|
||||
</label>
|
||||
<Field name="new_password" type="password" />
|
||||
<FormError name="new_password" />
|
||||
<div>
|
||||
<Field name="new_password" type="password" />
|
||||
<FormError name="new_password" />
|
||||
</div>
|
||||
</FormRow>
|
||||
<FormRow>
|
||||
<label htmlFor="confirm_password">
|
||||
<FormattedMessage id="label.confirm-password" defaultMessage="Confirm password" />
|
||||
</label>
|
||||
<Field name="confirm_password" type="password" />
|
||||
<FormError name="confirm_password" />
|
||||
<div>
|
||||
<Field name="confirm_password" type="password" />
|
||||
<FormError name="confirm_password" />
|
||||
</div>
|
||||
</FormRow>
|
||||
<FormButtons>
|
||||
<Button type="submit" variant="action">
|
||||
<FormattedMessage id="button.save" defaultMessage="Save" />
|
||||
<FormattedMessage id="label.save" defaultMessage="Save" />
|
||||
</Button>
|
||||
<Button onClick={onClose}>
|
||||
<FormattedMessage id="button.cancel" defaultMessage="Cancel" />
|
||||
<FormattedMessage id="label.cancel" defaultMessage="Cancel" />
|
||||
</Button>
|
||||
</FormButtons>
|
||||
<FormMessage>{message}</FormMessage>
|
||||
|
|
|
@ -33,11 +33,11 @@ export default function DatePickerForm({
|
|||
|
||||
const buttons = [
|
||||
{
|
||||
label: <FormattedMessage id="button.single-day" defaultMessage="Single day" />,
|
||||
label: <FormattedMessage id="label.single-day" defaultMessage="Single day" />,
|
||||
value: FILTER_DAY,
|
||||
},
|
||||
{
|
||||
label: <FormattedMessage id="button.date-range" defaultMessage="Date range" />,
|
||||
label: <FormattedMessage id="label.date-range" defaultMessage="Date range" />,
|
||||
value: FILTER_RANGE,
|
||||
},
|
||||
];
|
||||
|
@ -72,10 +72,10 @@ export default function DatePickerForm({
|
|||
</div>
|
||||
<FormButtons>
|
||||
<Button variant="action" onClick={handleSave} disabled={disabled}>
|
||||
<FormattedMessage id="button.save" defaultMessage="Save" />
|
||||
<FormattedMessage id="label.save" defaultMessage="Save" />
|
||||
</Button>
|
||||
<Button onClick={onClose}>
|
||||
<FormattedMessage id="button.cancel" defaultMessage="Cancel" />
|
||||
<FormattedMessage id="label.cancel" defaultMessage="Cancel" />
|
||||
</Button>
|
||||
</FormButtons>
|
||||
</div>
|
||||
|
|
|
@ -73,8 +73,10 @@ export default function DeleteForm({ values, onSave, onClose }) {
|
|||
/>
|
||||
</p>
|
||||
<FormRow>
|
||||
<Field name="confirmation" type="text" />
|
||||
<FormError name="confirmation" />
|
||||
<div>
|
||||
<Field name="confirmation" type="text" />
|
||||
<FormError name="confirmation" />
|
||||
</div>
|
||||
</FormRow>
|
||||
<FormButtons>
|
||||
<Button
|
||||
|
@ -82,10 +84,10 @@ export default function DeleteForm({ values, onSave, onClose }) {
|
|||
variant="danger"
|
||||
disabled={props.values.confirmation !== CONFIRMATION_WORD}
|
||||
>
|
||||
<FormattedMessage id="button.delete" defaultMessage="Delete" />
|
||||
<FormattedMessage id="label.delete" defaultMessage="Delete" />
|
||||
</Button>
|
||||
<Button onClick={onClose}>
|
||||
<FormattedMessage id="button.cancel" defaultMessage="Cancel" />
|
||||
<FormattedMessage id="label.cancel" defaultMessage="Cancel" />
|
||||
</Button>
|
||||
</FormButtons>
|
||||
<FormMessage>{message}</FormMessage>
|
||||
|
|
|
@ -71,19 +71,23 @@ export default function LoginForm() {
|
|||
<label htmlFor="username">
|
||||
<FormattedMessage id="label.username" defaultMessage="Username" />
|
||||
</label>
|
||||
<Field name="username" type="text" />
|
||||
<FormError name="username" />
|
||||
<div>
|
||||
<Field name="username" type="text" />
|
||||
<FormError name="username" />
|
||||
</div>
|
||||
</FormRow>
|
||||
<FormRow>
|
||||
<label htmlFor="password">
|
||||
<FormattedMessage id="label.password" defaultMessage="Password" />
|
||||
</label>
|
||||
<Field name="password" type="password" />
|
||||
<FormError name="password" />
|
||||
<div>
|
||||
<Field name="password" type="password" />
|
||||
<FormError name="password" />
|
||||
</div>
|
||||
</FormRow>
|
||||
<FormButtons>
|
||||
<Button type="submit" variant="action">
|
||||
<FormattedMessage id="button.login" defaultMessage="Login" />
|
||||
<FormattedMessage id="label.login" defaultMessage="Login" />
|
||||
</Button>
|
||||
</FormButtons>
|
||||
<FormMessage>{message}</FormMessage>
|
||||
|
|
|
@ -30,7 +30,7 @@ export default function TrackingCodeForm({ values, onClose }) {
|
|||
<FormButtons>
|
||||
<CopyButton type="submit" variant="action" element={ref} />
|
||||
<Button onClick={onClose}>
|
||||
<FormattedMessage id="button.cancel" defaultMessage="Cancel" />
|
||||
<FormattedMessage id="label.cancel" defaultMessage="Cancel" />
|
||||
</Button>
|
||||
</FormButtons>
|
||||
</FormLayout>
|
||||
|
|
|
@ -29,7 +29,7 @@ export default function TrackingCodeForm({ values, onClose }) {
|
|||
<FormButtons>
|
||||
<CopyButton type="submit" variant="action" element={ref} />
|
||||
<Button onClick={onClose}>
|
||||
<FormattedMessage id="button.cancel" defaultMessage="Cancel" />
|
||||
<FormattedMessage id="label.cancel" defaultMessage="Cancel" />
|
||||
</Button>
|
||||
</FormButtons>
|
||||
</FormLayout>
|
||||
|
|
|
@ -63,15 +63,19 @@ export default function WebsiteEditForm({ values, onSave, onClose }) {
|
|||
<label htmlFor="name">
|
||||
<FormattedMessage id="label.name" defaultMessage="Name" />
|
||||
</label>
|
||||
<Field name="name" type="text" />
|
||||
<FormError name="name" />
|
||||
<div>
|
||||
<Field name="name" type="text" />
|
||||
<FormError name="name" />
|
||||
</div>
|
||||
</FormRow>
|
||||
<FormRow>
|
||||
<label htmlFor="domain">
|
||||
<FormattedMessage id="label.domain" defaultMessage="Domain" />
|
||||
</label>
|
||||
<Field name="domain" type="text" />
|
||||
<FormError name="domain" />
|
||||
<div>
|
||||
<Field name="domain" type="text" />
|
||||
<FormError name="domain" />
|
||||
</div>
|
||||
</FormRow>
|
||||
<FormRow>
|
||||
<label></label>
|
||||
|
@ -91,10 +95,10 @@ export default function WebsiteEditForm({ values, onSave, onClose }) {
|
|||
</FormRow>
|
||||
<FormButtons>
|
||||
<Button type="submit" variant="action">
|
||||
<FormattedMessage id="button.save" defaultMessage="Save" />
|
||||
<FormattedMessage id="label.save" defaultMessage="Save" />
|
||||
</Button>
|
||||
<Button onClick={onClose}>
|
||||
<FormattedMessage id="button.cancel" defaultMessage="Cancel" />
|
||||
<FormattedMessage id="label.cancel" defaultMessage="Cancel" />
|
||||
</Button>
|
||||
</FormButtons>
|
||||
<FormMessage>{message}</FormMessage>
|
||||
|
|
|
@ -24,7 +24,9 @@ export default function Footer() {
|
|||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className={classNames(styles.version, 'col-12 col-md-4')}>{`v${current}`}</div>
|
||||
<div className={classNames(styles.version, 'col-12 col-md-4')}>
|
||||
<Link href={`https://github.com/mikecao/umami/releases`}>{`v${current}`}</Link>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
|
|
|
@ -17,6 +17,10 @@
|
|||
line-height: 1.8;
|
||||
}
|
||||
|
||||
.row > div {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.buttons {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
@ -33,9 +37,9 @@
|
|||
justify-content: center;
|
||||
align-items: center;
|
||||
top: 0;
|
||||
left: 100%;
|
||||
left: calc(100% + 16px);
|
||||
bottom: 0;
|
||||
margin-left: 16px;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.msg {
|
||||
|
@ -68,3 +72,15 @@
|
|||
color: var(--gray50);
|
||||
background: var(--gray800);
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 576px) {
|
||||
.error {
|
||||
align-items: flex-start;
|
||||
top: calc(100% + 7px);
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.error:after {
|
||||
left: 10px;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,31 @@
|
|||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import styles from './GridLayout.module.css';
|
||||
|
||||
export default function GridLayout({ className, children }) {
|
||||
return <div className={classNames(styles.grid, className)}>{children}</div>;
|
||||
}
|
||||
|
||||
export const GridRow = ({ className, children }) => {
|
||||
return <div className={classNames(styles.row, className, 'row')}>{children}</div>;
|
||||
};
|
||||
|
||||
export const GridColumn = ({ xs, sm, md, lg, xl, className, children }) => {
|
||||
const classes = [];
|
||||
|
||||
classes.push(xs ? `col-${xs}` : 'col-12');
|
||||
|
||||
if (sm) {
|
||||
classes.push(`col-sm-${sm}`);
|
||||
}
|
||||
if (md) {
|
||||
classes.push(`col-md-${md}`);
|
||||
}
|
||||
if (lg) {
|
||||
classes.push(`col-lg-${lg}`);
|
||||
}
|
||||
if (xl) {
|
||||
classes.push(`col-lg-${xl}`);
|
||||
}
|
||||
return <div className={classNames(styles.col, classes, className)}>{children}</div>;
|
||||
};
|
|
@ -0,0 +1,40 @@
|
|||
.grid {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.col {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.row {
|
||||
border-top: 1px solid var(--gray300);
|
||||
min-height: 430px;
|
||||
}
|
||||
|
||||
.row > .col {
|
||||
border-left: 1px solid var(--gray300);
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.row > .col:first-child {
|
||||
border-left: 0;
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
.row > .col:last-child {
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 992px) {
|
||||
.row {
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.row > .col {
|
||||
border-top: 1px solid var(--gray300);
|
||||
border-left: 0;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
|
@ -30,6 +30,9 @@ export default function Header() {
|
|||
<Link href="/dashboard">
|
||||
<FormattedMessage id="label.dashboard" defaultMessage="Dashboard" />
|
||||
</Link>
|
||||
<Link href="/realtime">
|
||||
<FormattedMessage id="label.realtime" defaultMessage="Realtime" />
|
||||
</Link>
|
||||
<Link href="/settings">
|
||||
<FormattedMessage id="label.settings" defaultMessage="Settings" />
|
||||
</Link>
|
||||
|
|
|
@ -1,11 +1,18 @@
|
|||
import React, { useMemo } from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import classNames from 'classnames';
|
||||
import useFetch from 'hooks/useFetch';
|
||||
import Dot from 'components/common/Dot';
|
||||
import { TOKEN_HEADER } from 'lib/constants';
|
||||
import useShareToken from 'hooks/useShareToken';
|
||||
import styles from './ActiveUsers.module.css';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
export default function ActiveUsers({ websiteId, token, className }) {
|
||||
const { data } = useFetch(`/api/website/${websiteId}/active`, { token }, { interval: 60000 });
|
||||
export default function ActiveUsers({ websiteId, className }) {
|
||||
const shareToken = useShareToken();
|
||||
const { data } = useFetch(`/api/website/${websiteId}/active`, {
|
||||
interval: 60000,
|
||||
headers: { [TOKEN_HEADER]: shareToken?.token },
|
||||
});
|
||||
const count = useMemo(() => {
|
||||
return data?.[0]?.x || 0;
|
||||
}, [data]);
|
||||
|
@ -16,7 +23,7 @@ export default function ActiveUsers({ websiteId, token, className }) {
|
|||
|
||||
return (
|
||||
<div className={classNames(styles.container, className)}>
|
||||
<div className={styles.dot} />
|
||||
<Dot />
|
||||
<div className={styles.text}>
|
||||
<div>
|
||||
<FormattedMessage
|
||||
|
|
|
@ -12,11 +12,3 @@
|
|||
font-weight: 600;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.dot {
|
||||
background: var(--green400);
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 100%;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
|
|
@ -1,21 +1,23 @@
|
|||
import React, { useState, useRef, useEffect } from 'react';
|
||||
import ReactTooltip from 'react-tooltip';
|
||||
import classNames from 'classnames';
|
||||
import ChartJS from 'chart.js';
|
||||
import Legend from 'components/metrics/Legend';
|
||||
import { formatLongNumber } from 'lib/format';
|
||||
import { dateFormat } from 'lib/lang';
|
||||
import useLocale from 'hooks/useLocale';
|
||||
import styles from './BarChart.module.css';
|
||||
import useTheme from 'hooks/useTheme';
|
||||
import { THEME_COLORS } from 'lib/constants';
|
||||
import { DEFAUL_CHART_HEIGHT, DEFAULT_ANIMATION_DURATION, THEME_COLORS } from 'lib/constants';
|
||||
import styles from './BarChart.module.css';
|
||||
import ChartTooltip from './ChartTooltip';
|
||||
import useForceUpdate from '../../hooks/useForceUpdate';
|
||||
|
||||
export default function BarChart({
|
||||
chartId,
|
||||
datasets,
|
||||
unit,
|
||||
records,
|
||||
height = 400,
|
||||
animationDuration = 300,
|
||||
height = DEFAUL_CHART_HEIGHT,
|
||||
animationDuration = DEFAULT_ANIMATION_DURATION,
|
||||
className,
|
||||
stacked = false,
|
||||
loading = false,
|
||||
|
@ -27,6 +29,8 @@ export default function BarChart({
|
|||
const [tooltip, setTooltip] = useState(null);
|
||||
const [locale] = useLocale();
|
||||
const [theme] = useTheme();
|
||||
const forceUpdate = useForceUpdate();
|
||||
|
||||
const colors = {
|
||||
text: THEME_COLORS[theme].gray700,
|
||||
line: THEME_COLORS[theme].gray200,
|
||||
|
@ -39,6 +43,8 @@ export default function BarChart({
|
|||
const w = canvas.current.width;
|
||||
|
||||
switch (unit) {
|
||||
case 'minute':
|
||||
return index % 2 === 0 ? dateFormat(d, 'h:mm', locale) : '';
|
||||
case 'hour':
|
||||
return dateFormat(d, 'ha', locale);
|
||||
case 'day':
|
||||
|
@ -63,7 +69,7 @@ export default function BarChart({
|
|||
}
|
||||
|
||||
function renderYLabel(label) {
|
||||
return +label > 1 ? formatLongNumber(label) : label;
|
||||
return +label > 1000 ? formatLongNumber(label) : label;
|
||||
}
|
||||
|
||||
function renderTooltip(model) {
|
||||
|
@ -109,9 +115,7 @@ export default function BarChart({
|
|||
responsiveAnimationDuration: 0,
|
||||
maintainAspectRatio: false,
|
||||
legend: {
|
||||
labels: {
|
||||
fontColor: colors.text,
|
||||
},
|
||||
display: false,
|
||||
},
|
||||
scales: {
|
||||
xAxes: [
|
||||
|
@ -177,6 +181,10 @@ export default function BarChart({
|
|||
options.tooltips.custom = renderTooltip;
|
||||
|
||||
onUpdate(chart.current);
|
||||
|
||||
chart.current.update();
|
||||
|
||||
forceUpdate();
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -200,23 +208,8 @@ export default function BarChart({
|
|||
>
|
||||
<canvas ref={canvas} />
|
||||
</div>
|
||||
<ReactTooltip id={`${chartId}-tooltip`}>
|
||||
{tooltip ? <Tooltip {...tooltip} /> : null}
|
||||
</ReactTooltip>
|
||||
<Legend chart={chart.current} />
|
||||
<ChartTooltip chartId={chartId} tooltip={tooltip} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const Tooltip = ({ title, value, label, labelColor }) => (
|
||||
<div className={styles.tooltip}>
|
||||
<div className={styles.content}>
|
||||
<div className={styles.title}>{title}</div>
|
||||
<div className={styles.metric}>
|
||||
<div className={styles.dot}>
|
||||
<div className={styles.color} style={{ backgroundColor: labelColor }} />
|
||||
</div>
|
||||
{value} {label}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -1,43 +1,3 @@
|
|||
.chart {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.tooltip {
|
||||
color: var(--msgColor);
|
||||
pointer-events: none;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: var(--font-size-xsmall);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.metric {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
font-size: var(--font-size-small);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.dot {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
border-radius: 100%;
|
||||
margin-right: 8px;
|
||||
background: var(--gray50);
|
||||
}
|
||||
|
||||
.color {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
}
|
||||
|
|
|
@ -3,15 +3,14 @@ import { FormattedMessage } from 'react-intl';
|
|||
import MetricsTable from './MetricsTable';
|
||||
import { browserFilter } from 'lib/filters';
|
||||
|
||||
export default function BrowsersTable({ websiteId, token, limit }) {
|
||||
export default function BrowsersTable({ websiteId, ...props }) {
|
||||
return (
|
||||
<MetricsTable
|
||||
{...props}
|
||||
title={<FormattedMessage id="metrics.browsers" defaultMessage="Browsers" />}
|
||||
type="browser"
|
||||
metric={<FormattedMessage id="metrics.visitors" defaultMessage="Visitors" />}
|
||||
websiteId={websiteId}
|
||||
token={token}
|
||||
limit={limit}
|
||||
dataFilter={browserFilter}
|
||||
/>
|
||||
);
|
||||
|
|
|
@ -0,0 +1,26 @@
|
|||
import React from 'react';
|
||||
import Dot from 'components/common/Dot';
|
||||
import styles from './ChartTooltip.module.css';
|
||||
import ReactTooltip from 'react-tooltip';
|
||||
|
||||
export default function ChartTooltip({ chartId, tooltip }) {
|
||||
if (!tooltip) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { title, value, label, labelColor } = tooltip;
|
||||
|
||||
return (
|
||||
<ReactTooltip id={`${chartId}-tooltip`}>
|
||||
<div className={styles.tooltip}>
|
||||
<div className={styles.content}>
|
||||
<div className={styles.title}>{title}</div>
|
||||
<div className={styles.metric}>
|
||||
<Dot color={labelColor} />
|
||||
{value} {label}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ReactTooltip>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,43 @@
|
|||
.chart {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.tooltip {
|
||||
color: var(--msgColor);
|
||||
pointer-events: none;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: var(--font-size-xsmall);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.metric {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
font-size: var(--font-size-small);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.dot {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
border-radius: 100%;
|
||||
margin-right: 8px;
|
||||
background: var(--gray50);
|
||||
}
|
||||
|
||||
.color {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
}
|
|
@ -5,7 +5,7 @@ import { FormattedMessage } from 'react-intl';
|
|||
import useCountryNames from 'hooks/useCountryNames';
|
||||
import useLocale from 'hooks/useLocale';
|
||||
|
||||
export default function CountriesTable({ websiteId, token, limit, onDataLoad = () => {} }) {
|
||||
export default function CountriesTable({ websiteId, onDataLoad, ...props }) {
|
||||
const [locale] = useLocale();
|
||||
const countryNames = useCountryNames(locale);
|
||||
|
||||
|
@ -15,13 +15,12 @@ export default function CountriesTable({ websiteId, token, limit, onDataLoad = (
|
|||
|
||||
return (
|
||||
<MetricsTable
|
||||
{...props}
|
||||
title={<FormattedMessage id="metrics.countries" defaultMessage="Countries" />}
|
||||
type="country"
|
||||
metric={<FormattedMessage id="metrics.visitors" defaultMessage="Visitors" />}
|
||||
websiteId={websiteId}
|
||||
token={token}
|
||||
limit={limit}
|
||||
onDataLoad={data => onDataLoad(percentFilter(data))}
|
||||
onDataLoad={data => onDataLoad?.(percentFilter(data))}
|
||||
renderLabel={renderLabel}
|
||||
/>
|
||||
);
|
||||
|
|
|
@ -0,0 +1,91 @@
|
|||
import React, { useState } from 'react';
|
||||
import { FixedSizeList } from 'react-window';
|
||||
import { useSpring, animated, config } from 'react-spring';
|
||||
import classNames from 'classnames';
|
||||
import NoData from 'components/common/NoData';
|
||||
import { formatNumber, formatLongNumber } from 'lib/format';
|
||||
import styles from './DataTable.module.css';
|
||||
|
||||
export default function DataTable({
|
||||
data,
|
||||
title,
|
||||
metric,
|
||||
className,
|
||||
renderLabel,
|
||||
height,
|
||||
animate = true,
|
||||
virtualize = false,
|
||||
}) {
|
||||
const [format, setFormat] = useState(true);
|
||||
const formatFunc = format ? formatLongNumber : formatNumber;
|
||||
|
||||
const handleSetFormat = () => setFormat(state => !state);
|
||||
|
||||
const getRow = row => {
|
||||
const { x: label, y: value, z: percent } = row;
|
||||
|
||||
return (
|
||||
<AnimatedRow
|
||||
key={label}
|
||||
label={renderLabel ? renderLabel(row) : label}
|
||||
value={value}
|
||||
percent={percent}
|
||||
animate={animate && !virtualize}
|
||||
format={formatFunc}
|
||||
onClick={handleSetFormat}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const Row = ({ index, style }) => {
|
||||
return <div style={style}>{getRow(data[index])}</div>;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={classNames(styles.table, className)}>
|
||||
<div className={styles.header}>
|
||||
<div className={styles.title}>{title}</div>
|
||||
<div className={styles.metric} onClick={handleSetFormat}>
|
||||
{metric}
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.body} style={{ height }}>
|
||||
{data?.length === 0 && <NoData />}
|
||||
{virtualize && data.length > 0 ? (
|
||||
<FixedSizeList height={height} itemCount={data.length} itemSize={30}>
|
||||
{Row}
|
||||
</FixedSizeList>
|
||||
) : (
|
||||
data.map(row => getRow(row))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const AnimatedRow = ({ label, value = 0, percent, animate, format, onClick }) => {
|
||||
const props = useSpring({
|
||||
width: percent,
|
||||
y: value,
|
||||
from: { width: 0, y: 0 },
|
||||
config: animate ? config.default : { duration: 0 },
|
||||
});
|
||||
|
||||
return (
|
||||
<div className={styles.row}>
|
||||
<div className={styles.label}>{label}</div>
|
||||
<div className={styles.value} onClick={onClick}>
|
||||
<animated.div className={styles.value}>{props.y?.interpolate(format)}</animated.div>
|
||||
</div>
|
||||
<div className={styles.percent}>
|
||||
<animated.div
|
||||
className={styles.bar}
|
||||
style={{ width: props.width.interpolate(n => `${n}%`) }}
|
||||
/>
|
||||
<animated.span className={styles.percentValue}>
|
||||
{props.width.interpolate(n => `${n.toFixed(0)}%`)}
|
||||
</animated.span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,95 @@
|
|||
.table {
|
||||
position: relative;
|
||||
font-size: var(--font-size-small);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.body {
|
||||
position: relative;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
line-height: 40px;
|
||||
}
|
||||
|
||||
.title {
|
||||
display: flex;
|
||||
font-weight: 600;
|
||||
font-size: var(--font-size-normal);
|
||||
}
|
||||
|
||||
.metric {
|
||||
font-size: var(--font-size-small);
|
||||
font-weight: 600;
|
||||
text-align: center;
|
||||
width: 100px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.row {
|
||||
position: relative;
|
||||
height: 30px;
|
||||
line-height: 30px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 5px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.label {
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
flex: 2;
|
||||
}
|
||||
|
||||
.label a {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.label a:hover {
|
||||
color: var(--primary400);
|
||||
}
|
||||
|
||||
.label:empty {
|
||||
color: #b3b3b3;
|
||||
}
|
||||
|
||||
.label:empty:before {
|
||||
content: 'Unknown';
|
||||
}
|
||||
|
||||
.value {
|
||||
width: 50px;
|
||||
text-align: right;
|
||||
margin-right: 10px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.percent {
|
||||
position: relative;
|
||||
width: 50px;
|
||||
color: var(--gray600);
|
||||
border-left: 1px solid var(--gray600);
|
||||
padding-left: 10px;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.bar {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 30px;
|
||||
opacity: 0.1;
|
||||
background: var(--primary400);
|
||||
z-index: -1;
|
||||
}
|
|
@ -4,15 +4,14 @@ import { deviceFilter } from 'lib/filters';
|
|||
import { FormattedMessage } from 'react-intl';
|
||||
import { getDeviceMessage } from 'components/messages';
|
||||
|
||||
export default function DevicesTable({ websiteId, token, limit }) {
|
||||
export default function DevicesTable({ websiteId, ...props }) {
|
||||
return (
|
||||
<MetricsTable
|
||||
{...props}
|
||||
title={<FormattedMessage id="metrics.devices" defaultMessage="Devices" />}
|
||||
type="device"
|
||||
metric={<FormattedMessage id="metrics.visitors" defaultMessage="Visitors" />}
|
||||
websiteId={websiteId}
|
||||
token={token}
|
||||
limit={limit}
|
||||
dataFilter={deviceFilter}
|
||||
renderLabel={({ x }) => getDeviceMessage(x)}
|
||||
/>
|
||||
|
|
|
@ -5,29 +5,36 @@ import { getDateArray, getDateLength } from 'lib/date';
|
|||
import useFetch from 'hooks/useFetch';
|
||||
import useDateRange from 'hooks/useDateRange';
|
||||
import useTimezone from 'hooks/useTimezone';
|
||||
import { EVENT_COLORS } from 'lib/constants';
|
||||
import usePageQuery from '../../hooks/usePageQuery';
|
||||
import usePageQuery from 'hooks/usePageQuery';
|
||||
import useShareToken from 'hooks/useShareToken';
|
||||
import { EVENT_COLORS, TOKEN_HEADER } from 'lib/constants';
|
||||
|
||||
export default function EventsChart({ websiteId, token }) {
|
||||
export default function EventsChart({ websiteId, className, token }) {
|
||||
const [dateRange] = useDateRange(websiteId);
|
||||
const { startDate, endDate, unit, modified } = dateRange;
|
||||
const [timezone] = useTimezone();
|
||||
const { query } = usePageQuery();
|
||||
const shareToken = useShareToken();
|
||||
|
||||
const { data } = useFetch(
|
||||
const { data, loading } = useFetch(
|
||||
`/api/website/${websiteId}/events`,
|
||||
{
|
||||
start_at: +startDate,
|
||||
end_at: +endDate,
|
||||
unit,
|
||||
tz: timezone,
|
||||
url: query.url,
|
||||
token,
|
||||
params: {
|
||||
start_at: +startDate,
|
||||
end_at: +endDate,
|
||||
unit,
|
||||
tz: timezone,
|
||||
url: query.url,
|
||||
token,
|
||||
},
|
||||
headers: { [TOKEN_HEADER]: shareToken?.token },
|
||||
},
|
||||
{ update: [modified] },
|
||||
[modified],
|
||||
);
|
||||
|
||||
const datasets = useMemo(() => {
|
||||
if (!data) return [];
|
||||
if (loading) return data;
|
||||
|
||||
const map = data.reduce((obj, { x, t, y }) => {
|
||||
if (!obj[x]) {
|
||||
|
@ -44,7 +51,7 @@ export default function EventsChart({ websiteId, token }) {
|
|||
});
|
||||
|
||||
return Object.keys(map).map((key, index) => {
|
||||
const color = tinycolor(EVENT_COLORS[index]);
|
||||
const color = tinycolor(EVENT_COLORS[index % EVENT_COLORS.length]);
|
||||
return {
|
||||
label: key,
|
||||
data: map[key],
|
||||
|
@ -54,15 +61,7 @@ export default function EventsChart({ websiteId, token }) {
|
|||
borderWidth: 1,
|
||||
};
|
||||
});
|
||||
}, [data]);
|
||||
|
||||
function handleCreate(options) {
|
||||
const legend = {
|
||||
position: 'bottom',
|
||||
};
|
||||
|
||||
options.legend = legend;
|
||||
}
|
||||
}, [data, loading]);
|
||||
|
||||
function handleUpdate(chart) {
|
||||
chart.data.datasets = datasets;
|
||||
|
@ -77,11 +76,13 @@ export default function EventsChart({ websiteId, token }) {
|
|||
return (
|
||||
<BarChart
|
||||
chartId={`events-${websiteId}`}
|
||||
className={className}
|
||||
datasets={datasets}
|
||||
unit={unit}
|
||||
height={300}
|
||||
records={getDateLength(startDate, endDate, unit)}
|
||||
onCreate={handleCreate}
|
||||
onUpdate={handleUpdate}
|
||||
loading={loading}
|
||||
stacked
|
||||
/>
|
||||
);
|
||||
|
|
|
@ -1,19 +1,17 @@
|
|||
import React from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import MetricsTable from './MetricsTable';
|
||||
import styles from './EventsTable.module.css';
|
||||
import Tag from 'components/common/Tag';
|
||||
|
||||
export default function EventsTable({ websiteId, token, limit, onDataLoad }) {
|
||||
export default function EventsTable({ websiteId, ...props }) {
|
||||
return (
|
||||
<MetricsTable
|
||||
{...props}
|
||||
title={<FormattedMessage id="metrics.events" defaultMessage="Events" />}
|
||||
type="event"
|
||||
metric={<FormattedMessage id="metrics.actions" defaultMessage="Actions" />}
|
||||
websiteId={websiteId}
|
||||
token={token}
|
||||
limit={limit}
|
||||
renderLabel={({ x }) => <Label value={x} />}
|
||||
onDataLoad={onDataLoad}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -22,7 +20,7 @@ const Label = ({ value }) => {
|
|||
const [event, label] = value.split(':');
|
||||
return (
|
||||
<>
|
||||
<span className={styles.type}>{event}</span>
|
||||
<Tag>{event}</Tag>
|
||||
{label}
|
||||
</>
|
||||
);
|
||||
|
|
|
@ -0,0 +1,40 @@
|
|||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import Dot from 'components/common/Dot';
|
||||
import useLocale from 'hooks/useLocale';
|
||||
import styles from './Legend.module.css';
|
||||
import useForceUpdate from '../../hooks/useForceUpdate';
|
||||
|
||||
export default function Legend({ chart }) {
|
||||
const [locale] = useLocale();
|
||||
const forceUpdate = useForceUpdate();
|
||||
|
||||
function handleClick(index) {
|
||||
const meta = chart.getDatasetMeta(index);
|
||||
|
||||
meta.hidden = meta.hidden === null ? !chart.data.datasets[index].hidden : null;
|
||||
|
||||
chart.update();
|
||||
|
||||
forceUpdate();
|
||||
}
|
||||
|
||||
if (!chart?.legend?.legendItems.find(({ text }) => text)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.legend}>
|
||||
{chart.legend.legendItems.map(({ text, fillStyle, datasetIndex, hidden }) => (
|
||||
<div
|
||||
key={text}
|
||||
className={classNames(styles.label, { [styles.hidden]: hidden })}
|
||||
onClick={() => handleClick(datasetIndex)}
|
||||
>
|
||||
<Dot color={fillStyle} />
|
||||
<span className={locale}>{text}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
.legend {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: var(--font-size-xsmall);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.label + .label {
|
||||
margin-left: 20px;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
color: var(--gray400);
|
||||
}
|
|
@ -5,12 +5,15 @@ import Loading from 'components/common/Loading';
|
|||
import ErrorMessage from 'components/common/ErrorMessage';
|
||||
import useFetch from 'hooks/useFetch';
|
||||
import useDateRange from 'hooks/useDateRange';
|
||||
import { formatShortTime, formatNumber, formatLongNumber } from 'lib/format';
|
||||
import usePageQuery from 'hooks/usePageQuery';
|
||||
import useShareToken from 'hooks/useShareToken';
|
||||
import { formatShortTime, formatNumber, formatLongNumber } from 'lib/format';
|
||||
import { TOKEN_HEADER } from 'lib/constants';
|
||||
import MetricCard from './MetricCard';
|
||||
import styles from './MetricsBar.module.css';
|
||||
|
||||
export default function MetricsBar({ websiteId, token, className }) {
|
||||
export default function MetricsBar({ websiteId, className }) {
|
||||
const shareToken = useShareToken();
|
||||
const [dateRange] = useDateRange(websiteId);
|
||||
const { startDate, endDate, modified } = dateRange;
|
||||
const [format, setFormat] = useState(true);
|
||||
|
@ -19,16 +22,16 @@ export default function MetricsBar({ websiteId, token, className }) {
|
|||
} = usePageQuery();
|
||||
|
||||
const { data, error, loading } = useFetch(
|
||||
`/api/website/${websiteId}/metrics`,
|
||||
`/api/website/${websiteId}/stats`,
|
||||
{
|
||||
start_at: +startDate,
|
||||
end_at: +endDate,
|
||||
url,
|
||||
token,
|
||||
},
|
||||
{
|
||||
update: [modified],
|
||||
params: {
|
||||
start_at: +startDate,
|
||||
end_at: +endDate,
|
||||
url,
|
||||
},
|
||||
headers: { [TOKEN_HEADER]: shareToken?.token },
|
||||
},
|
||||
[url, modified],
|
||||
);
|
||||
|
||||
const formatFunc = format ? formatLongNumber : formatNumber;
|
||||
|
|
|
@ -1,34 +1,32 @@
|
|||
import React, { useState, useMemo } from 'react';
|
||||
import React, { useMemo } from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import { FixedSizeList } from 'react-window';
|
||||
import { useSpring, animated, config } from 'react-spring';
|
||||
import firstBy from 'thenby';
|
||||
import classNames from 'classnames';
|
||||
import Link from 'components/common/Link';
|
||||
import Loading from 'components/common/Loading';
|
||||
import NoData from 'components/common/NoData';
|
||||
import useFetch from 'hooks/useFetch';
|
||||
import Arrow from 'assets/arrow-right.svg';
|
||||
import { percentFilter } from 'lib/filters';
|
||||
import { formatNumber, formatLongNumber } from 'lib/format';
|
||||
import useDateRange from 'hooks/useDateRange';
|
||||
import usePageQuery from 'hooks/usePageQuery';
|
||||
import useShareToken from 'hooks/useShareToken';
|
||||
import ErrorMessage from 'components/common/ErrorMessage';
|
||||
import DataTable from './DataTable';
|
||||
import { DEFAULT_ANIMATION_DURATION, TOKEN_HEADER } from 'lib/constants';
|
||||
import styles from './MetricsTable.module.css';
|
||||
import ErrorMessage from '../common/ErrorMessage';
|
||||
|
||||
export default function MetricsTable({
|
||||
websiteId,
|
||||
websiteDomain,
|
||||
token,
|
||||
title,
|
||||
metric,
|
||||
type,
|
||||
className,
|
||||
dataFilter,
|
||||
filterOptions,
|
||||
limit,
|
||||
renderLabel,
|
||||
onDataLoad = () => {},
|
||||
onDataLoad,
|
||||
...props
|
||||
}) {
|
||||
const shareToken = useShareToken();
|
||||
const [dateRange] = useDateRange(websiteId);
|
||||
const { startDate, endDate, modified } = dateRange;
|
||||
const {
|
||||
|
@ -38,117 +36,51 @@ export default function MetricsTable({
|
|||
} = usePageQuery();
|
||||
|
||||
const { data, loading, error } = useFetch(
|
||||
`/api/website/${websiteId}/rankings`,
|
||||
`/api/website/${websiteId}/metrics`,
|
||||
{
|
||||
type,
|
||||
start_at: +startDate,
|
||||
end_at: +endDate,
|
||||
domain: websiteDomain,
|
||||
url,
|
||||
token,
|
||||
params: {
|
||||
type,
|
||||
start_at: +startDate,
|
||||
end_at: +endDate,
|
||||
domain: websiteDomain,
|
||||
url,
|
||||
},
|
||||
onDataLoad,
|
||||
delay: DEFAULT_ANIMATION_DURATION,
|
||||
headers: { [TOKEN_HEADER]: shareToken?.token },
|
||||
},
|
||||
{ onDataLoad, delay: 300, update: [modified] },
|
||||
[modified],
|
||||
);
|
||||
const [format, setFormat] = useState(true);
|
||||
const formatFunc = format ? formatLongNumber : formatNumber;
|
||||
const shouldAnimate = limit > 0;
|
||||
|
||||
const rankings = useMemo(() => {
|
||||
const filteredData = useMemo(() => {
|
||||
if (data) {
|
||||
const items = percentFilter(dataFilter ? dataFilter(data, filterOptions) : data);
|
||||
if (limit) {
|
||||
return items.filter((e, i) => i < limit);
|
||||
return items.filter((e, i) => i < limit).sort(firstBy('y', -1).thenBy('x'));
|
||||
}
|
||||
return items;
|
||||
return items.sort(firstBy('y', -1).thenBy('x'));
|
||||
}
|
||||
return [];
|
||||
}, [data, error, dataFilter, filterOptions]);
|
||||
|
||||
const handleSetFormat = () => setFormat(state => !state);
|
||||
|
||||
const getRow = row => {
|
||||
const { x: label, y: value, z: percent } = row;
|
||||
return (
|
||||
<AnimatedRow
|
||||
key={label}
|
||||
label={renderLabel ? renderLabel(row) : label}
|
||||
value={value}
|
||||
percent={percent}
|
||||
animate={shouldAnimate}
|
||||
format={formatFunc}
|
||||
onClick={handleSetFormat}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const Row = ({ index, style }) => {
|
||||
return <div style={style}>{getRow(rankings[index])}</div>;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={classNames(styles.container, className)}>
|
||||
{!data && loading && <Loading />}
|
||||
{error && <ErrorMessage />}
|
||||
{data && !error && (
|
||||
<>
|
||||
<div className={styles.header}>
|
||||
<div className={styles.title}>{title}</div>
|
||||
<div className={styles.metric} onClick={handleSetFormat}>
|
||||
{metric}
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.body}>
|
||||
{rankings?.length === 0 && <NoData />}
|
||||
{limit
|
||||
? rankings.map(row => getRow(row))
|
||||
: rankings.length > 0 && (
|
||||
<FixedSizeList height={500} itemCount={rankings.length} itemSize={30}>
|
||||
{Row}
|
||||
</FixedSizeList>
|
||||
)}
|
||||
</div>
|
||||
<div className={styles.footer}>
|
||||
{limit && (
|
||||
<Link
|
||||
icon={<Arrow />}
|
||||
href={router.pathname}
|
||||
as={resolve({ view: type })}
|
||||
size="small"
|
||||
iconRight
|
||||
>
|
||||
<FormattedMessage id="button.more" defaultMessage="More" />
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{data && !error && <DataTable {...props} data={filteredData} className={className} />}
|
||||
<div className={styles.footer}>
|
||||
{data && !error && limit && (
|
||||
<Link
|
||||
icon={<Arrow />}
|
||||
href={router.pathname}
|
||||
as={resolve({ view: type })}
|
||||
size="small"
|
||||
iconRight
|
||||
>
|
||||
<FormattedMessage id="label.more" defaultMessage="More" />
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const AnimatedRow = ({ label, value = 0, percent, animate, format, onClick }) => {
|
||||
const props = useSpring({
|
||||
width: percent,
|
||||
y: value,
|
||||
from: { width: 0, y: 0 },
|
||||
config: animate ? config.default : { duration: 0 },
|
||||
});
|
||||
|
||||
return (
|
||||
<div className={styles.row}>
|
||||
<div className={styles.label}>{label}</div>
|
||||
<div className={styles.value} onClick={onClick}>
|
||||
<animated.div className={styles.value}>{props.y?.interpolate(format)}</animated.div>
|
||||
</div>
|
||||
<div className={styles.percent}>
|
||||
<animated.div
|
||||
className={styles.bar}
|
||||
style={{ width: props.width.interpolate(n => `${n}%`) }}
|
||||
/>
|
||||
<animated.span className={styles.percentValue}>
|
||||
{props.width.interpolate(n => `${n.toFixed(0)}%`)}
|
||||
</animated.span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -6,95 +6,6 @@
|
|||
flex-direction: column;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
line-height: 40px;
|
||||
}
|
||||
|
||||
.title {
|
||||
display: flex;
|
||||
font-weight: 600;
|
||||
font-size: var(--font-size-normal);
|
||||
}
|
||||
|
||||
.metric {
|
||||
font-size: var(--font-size-small);
|
||||
font-weight: 600;
|
||||
text-align: center;
|
||||
width: 100px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.row {
|
||||
position: relative;
|
||||
height: 30px;
|
||||
line-height: 30px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 5px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.label {
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
flex: 2;
|
||||
}
|
||||
|
||||
.label a {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.label a:hover {
|
||||
color: var(--primary400);
|
||||
}
|
||||
|
||||
.label:empty {
|
||||
color: #b3b3b3;
|
||||
}
|
||||
|
||||
.label:empty:before {
|
||||
content: 'Unknown';
|
||||
}
|
||||
|
||||
.value {
|
||||
width: 50px;
|
||||
text-align: right;
|
||||
margin-right: 10px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.percent {
|
||||
position: relative;
|
||||
width: 50px;
|
||||
color: var(--gray600);
|
||||
border-left: 1px solid var(--gray600);
|
||||
padding-left: 10px;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.bar {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 30px;
|
||||
opacity: 0.1;
|
||||
background: var(--primary400);
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
.body {
|
||||
position: relative;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.footer {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
|
|
@ -3,15 +3,14 @@ import MetricsTable from './MetricsTable';
|
|||
import { osFilter } from 'lib/filters';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
export default function OSTable({ websiteId, token, limit }) {
|
||||
export default function OSTable({ websiteId, ...props }) {
|
||||
return (
|
||||
<MetricsTable
|
||||
{...props}
|
||||
title={<FormattedMessage id="metrics.operating-systems" defaultMessage="Operating system" />}
|
||||
type="os"
|
||||
metric={<FormattedMessage id="metrics.visitors" defaultMessage="Visitors" />}
|
||||
websiteId={websiteId}
|
||||
token={token}
|
||||
limit={limit}
|
||||
dataFilter={osFilter}
|
||||
/>
|
||||
);
|
||||
|
|
|
@ -2,15 +2,16 @@ import React, { useState } from 'react';
|
|||
import { FormattedMessage } from 'react-intl';
|
||||
import classNames from 'classnames';
|
||||
import Link from 'next/link';
|
||||
import ButtonGroup from 'components/common/ButtonGroup';
|
||||
import ButtonLayout from 'components/layout/ButtonLayout';
|
||||
import FilterButtons from 'components/common/FilterButtons';
|
||||
import { urlFilter } from 'lib/filters';
|
||||
import { FILTER_COMBINED, FILTER_RAW } from 'lib/constants';
|
||||
import usePageQuery from 'hooks/usePageQuery';
|
||||
import MetricsTable from './MetricsTable';
|
||||
import styles from './PagesTable.module.css';
|
||||
|
||||
export default function PagesTable({ websiteId, token, websiteDomain, limit, showFilters }) {
|
||||
export const FILTER_COMBINED = 0;
|
||||
export const FILTER_RAW = 1;
|
||||
|
||||
export default function PagesTable({ websiteId, websiteDomain, showFilters, ...props }) {
|
||||
const [filter, setFilter] = useState(FILTER_COMBINED);
|
||||
const {
|
||||
resolve,
|
||||
|
@ -48,20 +49,11 @@ export default function PagesTable({ websiteId, token, websiteDomain, limit, sho
|
|||
type="url"
|
||||
metric={<FormattedMessage id="metrics.views" defaultMessage="Views" />}
|
||||
websiteId={websiteId}
|
||||
token={token}
|
||||
limit={limit}
|
||||
dataFilter={urlFilter}
|
||||
filterOptions={{ domain: websiteDomain, raw: filter === FILTER_RAW }}
|
||||
renderLabel={renderLink}
|
||||
{...props}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const FilterButtons = ({ buttons, selected, onClick }) => {
|
||||
return (
|
||||
<ButtonLayout>
|
||||
<ButtonGroup size="xsmall" items={buttons} selectedItem={selected} onClick={onClick} />
|
||||
</ButtonLayout>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -4,9 +4,18 @@ import tinycolor from 'tinycolor2';
|
|||
import CheckVisible from 'components/helpers/CheckVisible';
|
||||
import BarChart from './BarChart';
|
||||
import useTheme from 'hooks/useTheme';
|
||||
import { THEME_COLORS } from 'lib/constants';
|
||||
import { THEME_COLORS, DEFAULT_ANIMATION_DURATION } from 'lib/constants';
|
||||
|
||||
export default function PageviewsChart({ websiteId, data, unit, records, className, loading }) {
|
||||
export default function PageviewsChart({
|
||||
websiteId,
|
||||
data,
|
||||
unit,
|
||||
records,
|
||||
className,
|
||||
loading,
|
||||
animationDuration = DEFAULT_ANIMATION_DURATION,
|
||||
...props
|
||||
}) {
|
||||
const intl = useIntl();
|
||||
const [theme] = useTheme();
|
||||
const primaryColor = tinycolor(THEME_COLORS[theme].primary);
|
||||
|
@ -26,7 +35,7 @@ export default function PageviewsChart({ websiteId, data, unit, records, classNa
|
|||
data: { datasets },
|
||||
} = chart;
|
||||
|
||||
datasets[0].data = data.uniques;
|
||||
datasets[0].data = data.sessions;
|
||||
datasets[0].label = intl.formatMessage({
|
||||
id: 'metrics.unique-visitors',
|
||||
defaultMessage: 'Unique visitors',
|
||||
|
@ -36,8 +45,6 @@ export default function PageviewsChart({ websiteId, data, unit, records, classNa
|
|||
id: 'metrics.page-views',
|
||||
defaultMessage: 'Page views',
|
||||
});
|
||||
|
||||
chart.update();
|
||||
};
|
||||
|
||||
if (!data) {
|
||||
|
@ -48,6 +55,7 @@ export default function PageviewsChart({ websiteId, data, unit, records, classNa
|
|||
<CheckVisible>
|
||||
{visible => (
|
||||
<BarChart
|
||||
{...props}
|
||||
className={className}
|
||||
chartId={websiteId}
|
||||
datasets={[
|
||||
|
@ -56,7 +64,7 @@ export default function PageviewsChart({ websiteId, data, unit, records, classNa
|
|||
id: 'metrics.unique-visitors',
|
||||
defaultMessage: 'Unique visitors',
|
||||
}),
|
||||
data: data.uniques,
|
||||
data: data.sessions,
|
||||
lineTension: 0,
|
||||
backgroundColor: colors.visitors.background,
|
||||
borderColor: colors.visitors.border,
|
||||
|
@ -76,7 +84,7 @@ export default function PageviewsChart({ websiteId, data, unit, records, classNa
|
|||
]}
|
||||
unit={unit}
|
||||
records={records}
|
||||
animationDuration={visible ? 300 : 0}
|
||||
animationDuration={visible ? animationDuration : 0}
|
||||
onUpdate={handleUpdate}
|
||||
loading={loading}
|
||||
/>
|
||||
|
|
|
@ -0,0 +1,60 @@
|
|||
import React, { useMemo, useRef } from 'react';
|
||||
import { format, parseISO, startOfMinute, subMinutes, isBefore } from 'date-fns';
|
||||
import PageviewsChart from './PageviewsChart';
|
||||
import { getDateArray } from 'lib/date';
|
||||
import { DEFAULT_ANIMATION_DURATION, REALTIME_RANGE } from 'lib/constants';
|
||||
|
||||
function mapData(data) {
|
||||
let last = 0;
|
||||
const arr = [];
|
||||
|
||||
data.reduce((obj, val) => {
|
||||
const { created_at } = val;
|
||||
const t = startOfMinute(parseISO(created_at));
|
||||
if (t.getTime() > last) {
|
||||
obj = { t: format(t, 'yyyy-LL-dd HH:mm:00'), y: 1 };
|
||||
arr.push(obj);
|
||||
last = t;
|
||||
} else {
|
||||
obj.y += 1;
|
||||
}
|
||||
return obj;
|
||||
}, {});
|
||||
|
||||
return arr;
|
||||
}
|
||||
|
||||
export default function RealtimeChart({ data, unit, ...props }) {
|
||||
const endDate = startOfMinute(new Date());
|
||||
const startDate = subMinutes(endDate, REALTIME_RANGE);
|
||||
const prevEndDate = useRef(endDate);
|
||||
|
||||
const chartData = useMemo(() => {
|
||||
if (data) {
|
||||
return {
|
||||
pageviews: getDateArray(mapData(data.pageviews), startDate, endDate, unit),
|
||||
sessions: getDateArray(mapData(data.sessions), startDate, endDate, unit),
|
||||
};
|
||||
}
|
||||
return { pageviews: [], sessions: [] };
|
||||
}, [data]);
|
||||
|
||||
// Don't animate the bars shifting over because it looks weird
|
||||
const animationDuration = useMemo(() => {
|
||||
if (isBefore(prevEndDate.current, endDate)) {
|
||||
prevEndDate.current = endDate;
|
||||
return 0;
|
||||
}
|
||||
return DEFAULT_ANIMATION_DURATION;
|
||||
}, [data]);
|
||||
|
||||
return (
|
||||
<PageviewsChart
|
||||
{...props}
|
||||
height={200}
|
||||
unit={unit}
|
||||
data={chartData}
|
||||
animationDuration={animationDuration}
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,49 @@
|
|||
import React from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import PageHeader from '../layout/PageHeader';
|
||||
import DropDown from '../common/DropDown';
|
||||
import MetricCard from './MetricCard';
|
||||
import styles from './RealtimeHeader.module.css';
|
||||
|
||||
export default function RealtimeHeader({ websites, data, websiteId, onSelect }) {
|
||||
const options = [
|
||||
{ label: <FormattedMessage id="label.all-websites" defaultMessage="All websites" />, value: 0 },
|
||||
].concat(
|
||||
websites.map(({ name, website_id }, index) => ({
|
||||
label: name,
|
||||
value: website_id,
|
||||
divider: index === 0,
|
||||
})),
|
||||
);
|
||||
|
||||
const { pageviews, sessions, events, countries } = data;
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageHeader>
|
||||
<div>
|
||||
<FormattedMessage id="label.realtime" defaultMessage="Realtime" />
|
||||
</div>
|
||||
<DropDown value={websiteId} options={options} onChange={onSelect} />
|
||||
</PageHeader>
|
||||
<div className={styles.metrics}>
|
||||
<MetricCard
|
||||
label={<FormattedMessage id="metrics.views" defaultMessage="Views" />}
|
||||
value={pageviews.length}
|
||||
/>
|
||||
<MetricCard
|
||||
label={<FormattedMessage id="metrics.visitors" defaultMessage="Visitors" />}
|
||||
value={sessions.length}
|
||||
/>
|
||||
<MetricCard
|
||||
label={<FormattedMessage id="metrics.events" defaultMessage="Events" />}
|
||||
value={events.length}
|
||||
/>
|
||||
<MetricCard
|
||||
label={<FormattedMessage id="metrics.countries" defaultMessage="Countries" />}
|
||||
value={countries.length}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
.metrics {
|
||||
display: flex;
|
||||
margin-bottom: 10px;
|
||||
}
|
|
@ -0,0 +1,183 @@
|
|||
import React, { useMemo, useState } from 'react';
|
||||
import { FormattedMessage, useIntl } from 'react-intl';
|
||||
import { FixedSizeList } from 'react-window';
|
||||
import firstBy from 'thenby';
|
||||
import { format } from 'date-fns';
|
||||
import Icon from 'components/common/Icon';
|
||||
import Tag from 'components/common/Tag';
|
||||
import Dot from 'components/common/Dot';
|
||||
import FilterButtons from 'components/common/FilterButtons';
|
||||
import { devices } from 'components/messages';
|
||||
import useLocale from 'hooks/useLocale';
|
||||
import useCountryNames from 'hooks/useCountryNames';
|
||||
import { BROWSERS } from 'lib/constants';
|
||||
import Bolt from 'assets/bolt.svg';
|
||||
import Visitor from 'assets/visitor.svg';
|
||||
import Eye from 'assets/eye.svg';
|
||||
import { stringToColor } from 'lib/format';
|
||||
import styles from './RealtimeLog.module.css';
|
||||
import NoData from '../common/NoData';
|
||||
|
||||
const TYPE_ALL = 0;
|
||||
const TYPE_PAGEVIEW = 1;
|
||||
const TYPE_SESSION = 2;
|
||||
const TYPE_EVENT = 3;
|
||||
|
||||
const TYPE_ICONS = {
|
||||
[TYPE_PAGEVIEW]: <Eye />,
|
||||
[TYPE_SESSION]: <Visitor />,
|
||||
[TYPE_EVENT]: <Bolt />,
|
||||
};
|
||||
|
||||
export default function RealtimeLog({ data, websites }) {
|
||||
const intl = useIntl();
|
||||
const [locale] = useLocale();
|
||||
const countryNames = useCountryNames(locale);
|
||||
const [filter, setFilter] = useState(TYPE_ALL);
|
||||
|
||||
const logs = useMemo(() => {
|
||||
const { pageviews, sessions, events } = data;
|
||||
const logs = [...pageviews, ...sessions, ...events].sort(firstBy('created_at', -1));
|
||||
if (filter) {
|
||||
return logs.filter(row => getType(row) === filter);
|
||||
}
|
||||
return logs;
|
||||
}, [data, filter]);
|
||||
|
||||
const uuids = useMemo(() => {
|
||||
return data.sessions.reduce((obj, { session_id, session_uuid }) => {
|
||||
obj[session_id] = session_uuid;
|
||||
return obj;
|
||||
}, {});
|
||||
}, [data]);
|
||||
|
||||
const buttons = [
|
||||
{
|
||||
label: <FormattedMessage id="label.all" defaultMessage="All" />,
|
||||
value: TYPE_ALL,
|
||||
},
|
||||
{
|
||||
label: <FormattedMessage id="metrics.views" defaultMessage="Views" />,
|
||||
value: TYPE_PAGEVIEW,
|
||||
},
|
||||
{
|
||||
label: <FormattedMessage id="metrics.visitors" defaultMessage="Visitors" />,
|
||||
value: TYPE_SESSION,
|
||||
},
|
||||
{
|
||||
label: <FormattedMessage id="metrics.events" defaultMessage="Events" />,
|
||||
value: TYPE_EVENT,
|
||||
},
|
||||
];
|
||||
|
||||
function getType({ view_id, session_id, event_id }) {
|
||||
if (event_id) {
|
||||
return TYPE_EVENT;
|
||||
}
|
||||
if (view_id) {
|
||||
return TYPE_PAGEVIEW;
|
||||
}
|
||||
if (session_id) {
|
||||
return TYPE_SESSION;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function getIcon(row) {
|
||||
return TYPE_ICONS[getType(row)];
|
||||
}
|
||||
|
||||
function getWebsite({ website_id }) {
|
||||
return websites.find(n => n.website_id === website_id)?.name;
|
||||
}
|
||||
|
||||
function getDetail({
|
||||
event_type,
|
||||
event_value,
|
||||
view_id,
|
||||
session_id,
|
||||
url,
|
||||
browser,
|
||||
os,
|
||||
country,
|
||||
device,
|
||||
website_id,
|
||||
}) {
|
||||
if (event_type) {
|
||||
return (
|
||||
<div>
|
||||
<Tag>{event_type}</Tag> {event_value}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (view_id) {
|
||||
const domain = getWebsite({ website_id });
|
||||
return (
|
||||
<a
|
||||
className={styles.link}
|
||||
href={`//${domain}${url}`}
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
>
|
||||
{url}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
if (session_id) {
|
||||
return (
|
||||
<FormattedMessage
|
||||
id="message.log.visitor"
|
||||
defaultMessage="Visitor from {country} using {browser} on {os} {device}"
|
||||
values={{
|
||||
country: <b>{countryNames[country]}</b>,
|
||||
browser: <b>{BROWSERS[browser]}</b>,
|
||||
os: <b>{os}</b>,
|
||||
device: <b>{intl.formatMessage(devices[device])?.toLowerCase()}</b>,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function getTime({ created_at }) {
|
||||
return format(new Date(created_at), 'h:mm:ss');
|
||||
}
|
||||
|
||||
function getColor(row) {
|
||||
const { session_id } = row;
|
||||
|
||||
return stringToColor(uuids[session_id] || `${session_id}${getWebsite(row)}`);
|
||||
}
|
||||
|
||||
const Row = ({ index, style }) => {
|
||||
const row = logs[index];
|
||||
return (
|
||||
<div className={styles.row} style={style}>
|
||||
<div>
|
||||
<Dot color={getColor(row)} />
|
||||
</div>
|
||||
<div className={styles.time}>{getTime(row)}</div>
|
||||
<div className={styles.detail}>
|
||||
<Icon className={styles.icon} icon={getIcon(row)} />
|
||||
{getDetail(row)}
|
||||
</div>
|
||||
<div className={styles.website}>{getWebsite(row)}</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.table}>
|
||||
<FilterButtons buttons={buttons} selected={filter} onClick={setFilter} />
|
||||
<div className={styles.header}>
|
||||
<FormattedMessage id="label.realtime-logs" defaultMessage="Realtime logs" />
|
||||
</div>
|
||||
<div className={styles.body}>
|
||||
{logs?.length === 0 && <NoData />}
|
||||
<FixedSizeList height={400} itemCount={logs.length} itemSize={40}>
|
||||
{Row}
|
||||
</FixedSizeList>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,55 @@
|
|||
.table {
|
||||
font-size: var(--font-size-xsmall);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
font-size: 16px;
|
||||
line-height: 40px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 40px;
|
||||
border-bottom: 1px solid var(--gray300);
|
||||
}
|
||||
|
||||
.body {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.icon {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.time {
|
||||
min-width: 60px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.website {
|
||||
text-align: right;
|
||||
padding: 0 20px;
|
||||
}
|
||||
|
||||
.detail {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.row .link {
|
||||
color: var(--gray900);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.row .link:hover {
|
||||
color: var(--primary400);
|
||||
}
|
|
@ -0,0 +1,100 @@
|
|||
import React, { useMemo, useState, useCallback } from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import firstBy from 'thenby';
|
||||
import { percentFilter } from 'lib/filters';
|
||||
import DataTable from './DataTable';
|
||||
import FilterButtons from 'components/common/FilterButtons';
|
||||
|
||||
const FILTER_REFERRERS = 0;
|
||||
const FILTER_PAGES = 1;
|
||||
|
||||
export default function RealtimeViews({ websiteId, data, websites }) {
|
||||
const { pageviews } = data;
|
||||
const [filter, setFilter] = useState(FILTER_REFERRERS);
|
||||
const domains = useMemo(() => websites.map(({ domain }) => domain), [websites]);
|
||||
const getDomain = useCallback(
|
||||
id => websites.find(({ website_id }) => website_id === id)?.domain,
|
||||
[websites],
|
||||
);
|
||||
|
||||
const buttons = [
|
||||
{
|
||||
label: <FormattedMessage id="metrics.referrers" defaultMessage="Referrers" />,
|
||||
value: FILTER_REFERRERS,
|
||||
},
|
||||
{
|
||||
label: <FormattedMessage id="metrics.pages" defaultMessage="Pages" />,
|
||||
value: FILTER_PAGES,
|
||||
},
|
||||
];
|
||||
|
||||
const [referrers, pages] = useMemo(() => {
|
||||
if (pageviews) {
|
||||
const referrers = percentFilter(
|
||||
pageviews
|
||||
.reduce((arr, { referrer }) => {
|
||||
if (referrer?.startsWith('http')) {
|
||||
const hostname = new URL(referrer).hostname.replace(/^www\./, '');
|
||||
|
||||
if (hostname && !domains.includes(hostname)) {
|
||||
const row = arr.find(({ x }) => x === hostname);
|
||||
|
||||
if (!row) {
|
||||
arr.push({ x: hostname, y: 1 });
|
||||
} else {
|
||||
row.y += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
return arr;
|
||||
}, [])
|
||||
.sort(firstBy('y', -1)),
|
||||
);
|
||||
|
||||
const pages = percentFilter(
|
||||
pageviews
|
||||
.reduce((arr, { url, website_id }) => {
|
||||
if (url?.startsWith('/')) {
|
||||
if (!websiteId) {
|
||||
url = `${getDomain(website_id)}${url}`;
|
||||
}
|
||||
const row = arr.find(({ x }) => x === url);
|
||||
|
||||
if (!row) {
|
||||
arr.push({ x: url, y: 1 });
|
||||
} else {
|
||||
row.y += 1;
|
||||
}
|
||||
}
|
||||
return arr;
|
||||
}, [])
|
||||
.sort(firstBy('y', -1)),
|
||||
);
|
||||
|
||||
return [referrers, pages];
|
||||
}
|
||||
return [];
|
||||
}, [pageviews]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<FilterButtons buttons={buttons} selected={filter} onClick={setFilter} />
|
||||
{filter === FILTER_REFERRERS && (
|
||||
<DataTable
|
||||
title={<FormattedMessage id="metrics.referrers" defaultMessage="Referrers" />}
|
||||
metric={<FormattedMessage id="metrics.views" defaultMessage="Views" />}
|
||||
data={referrers}
|
||||
height={400}
|
||||
/>
|
||||
)}
|
||||
{filter === FILTER_PAGES && (
|
||||
<DataTable
|
||||
title={<FormattedMessage id="metrics.pages" defaultMessage="Pages" />}
|
||||
metric={<FormattedMessage id="metrics.views" defaultMessage="Views" />}
|
||||
data={pages}
|
||||
height={400}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -1,12 +1,14 @@
|
|||
import React, { useState } from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import MetricsTable from './MetricsTable';
|
||||
import FilterButtons from 'components/common/FilterButtons';
|
||||
import { refFilter } from 'lib/filters';
|
||||
import ButtonGroup from 'components/common/ButtonGroup';
|
||||
import { FILTER_DOMAIN_ONLY, FILTER_COMBINED, FILTER_RAW } from 'lib/constants';
|
||||
import ButtonLayout from '../layout/ButtonLayout';
|
||||
|
||||
export default function ReferrersTable({ websiteId, websiteDomain, token, limit, showFilters }) {
|
||||
export const FILTER_DOMAIN_ONLY = 0;
|
||||
export const FILTER_COMBINED = 1;
|
||||
export const FILTER_RAW = 2;
|
||||
|
||||
export default function ReferrersTable({ websiteId, websiteDomain, showFilters, ...props }) {
|
||||
const [filter, setFilter] = useState(FILTER_COMBINED);
|
||||
|
||||
const buttons = [
|
||||
|
@ -35,13 +37,12 @@ export default function ReferrersTable({ websiteId, websiteDomain, token, limit,
|
|||
<>
|
||||
{showFilters && <FilterButtons buttons={buttons} selected={filter} onClick={setFilter} />}
|
||||
<MetricsTable
|
||||
{...props}
|
||||
title={<FormattedMessage id="metrics.referrers" defaultMessage="Referrers" />}
|
||||
type="referrer"
|
||||
metric={<FormattedMessage id="metrics.views" defaultMessage="Views" />}
|
||||
websiteId={websiteId}
|
||||
websiteDomain={websiteDomain}
|
||||
token={token}
|
||||
limit={limit}
|
||||
dataFilter={refFilter}
|
||||
filterOptions={{
|
||||
domain: websiteDomain,
|
||||
|
@ -53,11 +54,3 @@ export default function ReferrersTable({ websiteId, websiteDomain, token, limit,
|
|||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const FilterButtons = ({ buttons, selected, onClick }) => {
|
||||
return (
|
||||
<ButtonLayout>
|
||||
<ButtonGroup size="xsmall" items={buttons} selectedItem={selected} onClick={onClick} />
|
||||
</ButtonLayout>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -14,15 +14,18 @@ import { getDateArray, getDateLength } from 'lib/date';
|
|||
import Times from 'assets/times.svg';
|
||||
import styles from './WebsiteChart.module.css';
|
||||
import ErrorMessage from '../common/ErrorMessage';
|
||||
import useShareToken from '../../hooks/useShareToken';
|
||||
import { TOKEN_HEADER } from '../../lib/constants';
|
||||
|
||||
export default function WebsiteChart({
|
||||
websiteId,
|
||||
token,
|
||||
title,
|
||||
domain,
|
||||
stickyHeader = false,
|
||||
showLink = false,
|
||||
onDataLoad = () => {},
|
||||
}) {
|
||||
const shareToken = useShareToken();
|
||||
const [dateRange, setDateRange] = useDateRange(websiteId);
|
||||
const { startDate, endDate, unit, value, modified } = dateRange;
|
||||
const [timezone] = useTimezone();
|
||||
|
@ -35,24 +38,27 @@ export default function WebsiteChart({
|
|||
const { data, loading, error } = useFetch(
|
||||
`/api/website/${websiteId}/pageviews`,
|
||||
{
|
||||
start_at: +startDate,
|
||||
end_at: +endDate,
|
||||
unit,
|
||||
tz: timezone,
|
||||
url,
|
||||
token,
|
||||
params: {
|
||||
start_at: +startDate,
|
||||
end_at: +endDate,
|
||||
unit,
|
||||
tz: timezone,
|
||||
url,
|
||||
},
|
||||
onDataLoad,
|
||||
headers: { [TOKEN_HEADER]: shareToken?.token },
|
||||
},
|
||||
{ onDataLoad, update: [modified] },
|
||||
[url, modified],
|
||||
);
|
||||
|
||||
const [pageviews, uniques] = useMemo(() => {
|
||||
const chartData = useMemo(() => {
|
||||
if (data) {
|
||||
return [
|
||||
getDateArray(data.pageviews, startDate, endDate, unit),
|
||||
getDateArray(data.uniques, startDate, endDate, unit),
|
||||
];
|
||||
return {
|
||||
pageviews: getDateArray(data.pageviews, startDate, endDate, unit),
|
||||
sessions: getDateArray(data.sessions, startDate, endDate, unit),
|
||||
};
|
||||
}
|
||||
return [[], []];
|
||||
return { pageviews: [], sessions: [] };
|
||||
}, [data]);
|
||||
|
||||
function handleCloseFilter() {
|
||||
|
@ -61,7 +67,7 @@ export default function WebsiteChart({
|
|||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<WebsiteHeader websiteId={websiteId} token={token} title={title} showLink={showLink} />
|
||||
<WebsiteHeader websiteId={websiteId} title={title} domain={domain} showLink={showLink} />
|
||||
<div className={classNames(styles.header, 'row')}>
|
||||
<StickyHeader
|
||||
className={classNames(styles.metrics, 'col row')}
|
||||
|
@ -70,7 +76,7 @@ export default function WebsiteChart({
|
|||
>
|
||||
{url && <PageFilter url={url} onClick={handleCloseFilter} />}
|
||||
<div className="col-12 col-lg-9">
|
||||
<MetricsBar websiteId={websiteId} token={token} />
|
||||
<MetricsBar websiteId={websiteId} />
|
||||
</div>
|
||||
<div className={classNames(styles.filter, 'col-12 col-lg-3')}>
|
||||
<DateFilter
|
||||
|
@ -87,7 +93,7 @@ export default function WebsiteChart({
|
|||
{error && <ErrorMessage />}
|
||||
<PageviewsChart
|
||||
websiteId={websiteId}
|
||||
data={{ pageviews, uniques }}
|
||||
data={chartData}
|
||||
unit={unit}
|
||||
records={getDateLength(startDate, endDate, unit)}
|
||||
loading={loading}
|
||||
|
|
|
@ -4,15 +4,19 @@ import Link from 'components/common/Link';
|
|||
import PageHeader from 'components/layout/PageHeader';
|
||||
import RefreshButton from 'components/common/RefreshButton';
|
||||
import ButtonLayout from 'components/layout/ButtonLayout';
|
||||
import Favicon from 'components/common/Favicon';
|
||||
import ActiveUsers from './ActiveUsers';
|
||||
import Arrow from 'assets/arrow-right.svg';
|
||||
import styles from './WebsiteHeader.module.css';
|
||||
|
||||
export default function WebsiteHeader({ websiteId, token, title, showLink = false }) {
|
||||
export default function WebsiteHeader({ websiteId, title, domain, showLink = false }) {
|
||||
return (
|
||||
<PageHeader>
|
||||
<div className={styles.title}>{title}</div>
|
||||
<ActiveUsers className={styles.active} websiteId={websiteId} token={token} />
|
||||
<div className={styles.title}>
|
||||
<Favicon domain={domain} />
|
||||
{title}
|
||||
</div>
|
||||
<ActiveUsers className={styles.active} websiteId={websiteId} />
|
||||
<ButtonLayout align="right">
|
||||
<RefreshButton websiteId={websiteId} />
|
||||
{showLink && (
|
||||
|
@ -24,7 +28,7 @@ export default function WebsiteHeader({ websiteId, token, title, showLink = fals
|
|||
size="small"
|
||||
iconRight
|
||||
>
|
||||
<FormattedMessage id="button.view-details" defaultMessage="View details" />
|
||||
<FormattedMessage id="label.view-details" defaultMessage="View details" />
|
||||
</Link>
|
||||
)}
|
||||
</ButtonLayout>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
.title {
|
||||
color: var(--gray-900);
|
||||
color: var(--gray900);
|
||||
font-size: var(--font-size-large);
|
||||
line-height: var(--font-size-large);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,158 @@
|
|||
import React, { useState, useEffect, useMemo, useCallback } from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import { subMinutes, startOfMinute } from 'date-fns';
|
||||
import firstBy from 'thenby';
|
||||
import Page from 'components/layout/Page';
|
||||
import GridLayout, { GridRow, GridColumn } from 'components/layout/GridLayout';
|
||||
import RealtimeChart from 'components/metrics/RealtimeChart';
|
||||
import RealtimeLog from 'components/metrics/RealtimeLog';
|
||||
import RealtimeHeader from 'components/metrics/RealtimeHeader';
|
||||
import WorldMap from 'components/common/WorldMap';
|
||||
import DataTable from 'components/metrics/DataTable';
|
||||
import RealtimeViews from 'components/metrics/RealtimeViews';
|
||||
import useFetch from 'hooks/useFetch';
|
||||
import useLocale from 'hooks/useLocale';
|
||||
import useCountryNames from 'hooks/useCountryNames';
|
||||
import { percentFilter } from 'lib/filters';
|
||||
import { TOKEN_HEADER, REALTIME_RANGE, REALTIME_INTERVAL } from 'lib/constants';
|
||||
import styles from './RealtimeDashboard.module.css';
|
||||
|
||||
function mergeData(state, data, time) {
|
||||
const ids = state.map(({ __id }) => __id);
|
||||
return state
|
||||
.concat(data.filter(({ __id }) => !ids.includes(__id)))
|
||||
.filter(({ created_at }) => new Date(created_at).getTime() >= time);
|
||||
}
|
||||
|
||||
function filterWebsite(data, id) {
|
||||
return data.filter(({ website_id }) => website_id === id);
|
||||
}
|
||||
|
||||
export default function RealtimeDashboard() {
|
||||
const [locale] = useLocale();
|
||||
const countryNames = useCountryNames(locale);
|
||||
const [data, setData] = useState();
|
||||
const [websiteId, setWebsiteId] = useState(0);
|
||||
const { data: init, loading } = useFetch('/api/realtime/init');
|
||||
const { data: updates } = useFetch('/api/realtime/update', {
|
||||
params: { start_at: data?.timestamp },
|
||||
disabled: !init?.websites?.length || !data,
|
||||
interval: REALTIME_INTERVAL,
|
||||
headers: { [TOKEN_HEADER]: init?.token },
|
||||
});
|
||||
|
||||
const renderCountryName = useCallback(
|
||||
({ x }) => <span className={locale}>{countryNames[x]}</span>,
|
||||
[countryNames],
|
||||
);
|
||||
|
||||
const realtimeData = useMemo(() => {
|
||||
if (data) {
|
||||
const { pageviews, sessions, events } = data;
|
||||
|
||||
if (websiteId) {
|
||||
return {
|
||||
pageviews: filterWebsite(pageviews, websiteId),
|
||||
sessions: filterWebsite(sessions, websiteId),
|
||||
events: filterWebsite(events, websiteId),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return data;
|
||||
}, [data, websiteId]);
|
||||
|
||||
const countries = useMemo(() => {
|
||||
if (realtimeData?.sessions) {
|
||||
return percentFilter(
|
||||
realtimeData.sessions
|
||||
.reduce((arr, { country }) => {
|
||||
if (country) {
|
||||
const row = arr.find(({ x }) => x === country);
|
||||
|
||||
if (!row) {
|
||||
arr.push({ x: country, y: 1 });
|
||||
} else {
|
||||
row.y += 1;
|
||||
}
|
||||
}
|
||||
return arr;
|
||||
}, [])
|
||||
.sort(firstBy('y', -1)),
|
||||
);
|
||||
}
|
||||
return [];
|
||||
}, [realtimeData?.sessions]);
|
||||
|
||||
useEffect(() => {
|
||||
if (init && !data) {
|
||||
const { websites, data } = init;
|
||||
|
||||
setData({ websites, ...data });
|
||||
}
|
||||
}, [init]);
|
||||
|
||||
useEffect(() => {
|
||||
if (updates) {
|
||||
const { pageviews, sessions, events, timestamp } = updates;
|
||||
const time = subMinutes(startOfMinute(new Date()), REALTIME_RANGE).getTime();
|
||||
|
||||
setData(state => ({
|
||||
...state,
|
||||
pageviews: mergeData(state.pageviews, pageviews, time),
|
||||
sessions: mergeData(state.sessions, sessions, time),
|
||||
events: mergeData(state.events, events, time),
|
||||
timestamp,
|
||||
}));
|
||||
}
|
||||
}, [updates]);
|
||||
|
||||
if (!init || !data || loading) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { websites } = data;
|
||||
|
||||
return (
|
||||
<Page>
|
||||
<RealtimeHeader
|
||||
websites={websites}
|
||||
websiteId={websiteId}
|
||||
data={{ ...realtimeData, countries }}
|
||||
onSelect={setWebsiteId}
|
||||
/>
|
||||
<div className={styles.chart}>
|
||||
<RealtimeChart
|
||||
websiteId={websiteId}
|
||||
data={realtimeData}
|
||||
unit="minute"
|
||||
records={REALTIME_RANGE}
|
||||
/>
|
||||
</div>
|
||||
<GridLayout>
|
||||
<GridRow>
|
||||
<GridColumn xs={12} lg={4}>
|
||||
<RealtimeViews websiteId={websiteId} data={realtimeData} websites={websites} />
|
||||
</GridColumn>
|
||||
<GridColumn xs={12} lg={8}>
|
||||
<RealtimeLog data={realtimeData} websites={websites} />
|
||||
</GridColumn>
|
||||
</GridRow>
|
||||
<GridRow>
|
||||
<GridColumn xs={12} lg={4}>
|
||||
<DataTable
|
||||
title={<FormattedMessage id="metrics.countries" defaultMessage="Countries" />}
|
||||
metric={<FormattedMessage id="metrics.visitors" defaultMessage="Visitors" />}
|
||||
data={countries}
|
||||
renderLabel={renderCountryName}
|
||||
height={500}
|
||||
/>
|
||||
</GridColumn>
|
||||
<GridColumn xs={12} lg={8}>
|
||||
<WorldMap data={countries} />
|
||||
</GridColumn>
|
||||
</GridRow>
|
||||
</GridLayout>
|
||||
</Page>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
.container {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.chart {
|
||||
margin-bottom: 30px;
|
||||
}
|
|
@ -7,7 +7,7 @@ import Page from '../layout/Page';
|
|||
import PageHeader from '../layout/PageHeader';
|
||||
import useFetch from '../../hooks/useFetch';
|
||||
import DropDown from '../common/DropDown';
|
||||
import styles from './Test.module.css';
|
||||
import styles from './TestConsole.module.css';
|
||||
import WebsiteChart from '../metrics/WebsiteChart';
|
||||
import EventsChart from '../metrics/EventsChart';
|
||||
import Button from '../common/Button';
|
||||
|
@ -82,7 +82,12 @@ export default function TestConsole() {
|
|||
</div>
|
||||
<div className="row">
|
||||
<div className="col-12">
|
||||
<WebsiteChart websiteId={website.website_id} title={website.name} showLink />
|
||||
<WebsiteChart
|
||||
websiteId={website.website_id}
|
||||
title={website.name}
|
||||
domain={website.domain}
|
||||
showLink
|
||||
/>
|
||||
<PageHeader>Events</PageHeader>
|
||||
<EventsChart websiteId={website.website_id} />
|
||||
</div>
|
||||
|
|
|
@ -4,6 +4,7 @@ import classNames from 'classnames';
|
|||
import WebsiteChart from 'components/metrics/WebsiteChart';
|
||||
import WorldMap from 'components/common/WorldMap';
|
||||
import Page from 'components/layout/Page';
|
||||
import GridLayout, { GridRow, GridColumn } from 'components/layout/GridLayout';
|
||||
import MenuLayout from 'components/layout/MenuLayout';
|
||||
import Link from 'components/common/Link';
|
||||
import Loading from 'components/common/Loading';
|
||||
|
@ -19,6 +20,8 @@ import EventsTable from '../metrics/EventsTable';
|
|||
import EventsChart from '../metrics/EventsChart';
|
||||
import useFetch from 'hooks/useFetch';
|
||||
import usePageQuery from 'hooks/usePageQuery';
|
||||
import useShareToken from 'hooks/useShareToken';
|
||||
import { DEFAULT_ANIMATION_DURATION, TOKEN_HEADER } from 'lib/constants';
|
||||
|
||||
const views = {
|
||||
url: PagesTable,
|
||||
|
@ -30,8 +33,11 @@ const views = {
|
|||
event: EventsTable,
|
||||
};
|
||||
|
||||
export default function WebsiteDetails({ websiteId, token }) {
|
||||
const { data } = useFetch(`/api/website/${websiteId}`, { token });
|
||||
export default function WebsiteDetails({ websiteId }) {
|
||||
const shareToken = useShareToken();
|
||||
const { data } = useFetch(`/api/website/${websiteId}`, {
|
||||
headers: { [TOKEN_HEADER]: shareToken?.token },
|
||||
});
|
||||
const [chartLoaded, setChartLoaded] = useState(false);
|
||||
const [countryData, setCountryData] = useState();
|
||||
const [eventsData, setEventsData] = useState();
|
||||
|
@ -50,7 +56,7 @@ export default function WebsiteDetails({ websiteId, token }) {
|
|||
icon={<Arrow />}
|
||||
size="small"
|
||||
>
|
||||
<FormattedMessage id="button.back" defaultMessage="Back" />
|
||||
<FormattedMessage id="label.back" defaultMessage="Back" />
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
|
@ -91,7 +97,6 @@ export default function WebsiteDetails({ websiteId, token }) {
|
|||
|
||||
const tableProps = {
|
||||
websiteId,
|
||||
token,
|
||||
websiteDomain: data?.domain,
|
||||
limit: 10,
|
||||
};
|
||||
|
@ -100,7 +105,7 @@ export default function WebsiteDetails({ websiteId, token }) {
|
|||
|
||||
function handleDataLoad() {
|
||||
if (!chartLoaded) {
|
||||
setTimeout(() => setChartLoaded(true), 300);
|
||||
setTimeout(() => setChartLoaded(true), DEFAULT_ANIMATION_DURATION);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -114,8 +119,8 @@ export default function WebsiteDetails({ websiteId, token }) {
|
|||
<div className={classNames(styles.chart, 'col')}>
|
||||
<WebsiteChart
|
||||
websiteId={websiteId}
|
||||
token={token}
|
||||
title={data.name}
|
||||
domain={data.domain}
|
||||
onDataLoad={handleDataLoad}
|
||||
showLink={false}
|
||||
stickyHeader
|
||||
|
@ -124,54 +129,59 @@ export default function WebsiteDetails({ websiteId, token }) {
|
|||
</div>
|
||||
{!chartLoaded && <Loading />}
|
||||
{chartLoaded && !view && (
|
||||
<>
|
||||
<div className={classNames(styles.row, 'row')}>
|
||||
<div className="col-md-12 col-lg-6">
|
||||
<GridLayout>
|
||||
<GridRow>
|
||||
<GridColumn md={12} lg={6}>
|
||||
<PagesTable {...tableProps} />
|
||||
</div>
|
||||
<div className="col-md-12 col-lg-6">
|
||||
</GridColumn>
|
||||
<GridColumn md={12} lg={6}>
|
||||
<ReferrersTable {...tableProps} />
|
||||
</div>
|
||||
</div>
|
||||
<div className={classNames(styles.row, 'row')}>
|
||||
<div className="col-md-12 col-lg-4">
|
||||
</GridColumn>
|
||||
</GridRow>
|
||||
<GridRow>
|
||||
<GridColumn md={12} lg={4}>
|
||||
<BrowsersTable {...tableProps} />
|
||||
</div>
|
||||
<div className="col-md-12 col-lg-4">
|
||||
</GridColumn>
|
||||
<GridColumn md={12} lg={4}>
|
||||
<OSTable {...tableProps} />
|
||||
</div>
|
||||
<div className="col-md-12 col-lg-4">
|
||||
</GridColumn>
|
||||
<GridColumn md={12} lg={4}>
|
||||
<DevicesTable {...tableProps} />
|
||||
</div>
|
||||
</div>
|
||||
<div className={classNames(styles.row, 'row')}>
|
||||
<div className="col-12 col-md-12 col-lg-8">
|
||||
</GridColumn>
|
||||
</GridRow>
|
||||
<GridRow>
|
||||
<GridColumn xs={12} md={12} lg={8}>
|
||||
<WorldMap data={countryData} />
|
||||
</div>
|
||||
<div className="col-12 col-md-12 col-lg-4">
|
||||
</GridColumn>
|
||||
<GridColumn xs={12} md={12} lg={4}>
|
||||
<CountriesTable {...tableProps} onDataLoad={setCountryData} />
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className={classNames(styles.row, 'row', { [styles.hidden]: !eventsData?.length > 0 })}
|
||||
>
|
||||
<div className="col-12 col-md-12 col-lg-4">
|
||||
</GridColumn>
|
||||
</GridRow>
|
||||
<GridRow className={classNames({ [styles.hidden]: !eventsData?.length > 0 })}>
|
||||
<GridColumn xs={12} md={12} lg={4}>
|
||||
<EventsTable {...tableProps} onDataLoad={setEventsData} />
|
||||
</div>
|
||||
<div className="col-12 col-md-12 col-lg-8 pt-5 pb-5">
|
||||
<EventsChart websiteId={websiteId} token={token} />
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
</GridColumn>
|
||||
<GridColumn xs={12} md={12} lg={8}>
|
||||
<EventsChart className={styles.eventschart} websiteId={websiteId} />
|
||||
</GridColumn>
|
||||
</GridRow>
|
||||
</GridLayout>
|
||||
)}
|
||||
{view && (
|
||||
{view && chartLoaded && (
|
||||
<MenuLayout
|
||||
className={styles.view}
|
||||
menuClassName={styles.menu}
|
||||
contentClassName={styles.content}
|
||||
menu={menuOptions}
|
||||
>
|
||||
<DetailsComponent {...tableProps} limit={false} showFilters={true} />
|
||||
<DetailsComponent
|
||||
{...tableProps}
|
||||
height={500}
|
||||
limit={false}
|
||||
animte={false}
|
||||
showFilters
|
||||
virtualize
|
||||
/>
|
||||
</MenuLayout>
|
||||
)}
|
||||
</Page>
|
||||
|
|
|
@ -26,37 +26,10 @@
|
|||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.row {
|
||||
border-top: 1px solid var(--gray300);
|
||||
min-height: 430px;
|
||||
}
|
||||
|
||||
.row > [class*='col-'] {
|
||||
border-left: 1px solid var(--gray300);
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.row > [class*='col-']:first-child {
|
||||
border-left: 0;
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
.row > [class*='col-']:last-child {
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 992px) {
|
||||
.row {
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.row > [class*='col-'] {
|
||||
border-top: 1px solid var(--gray300);
|
||||
border-left: 0;
|
||||
padding: 0;
|
||||
}
|
||||
.eventschart {
|
||||
padding-top: 30px;
|
||||
}
|
||||
|
|
|
@ -9,7 +9,7 @@ import Arrow from 'assets/arrow-right.svg';
|
|||
import styles from './WebsiteList.module.css';
|
||||
|
||||
export default function WebsiteList({ userId }) {
|
||||
const { data } = useFetch('/api/websites', { user_id: userId });
|
||||
const { data } = useFetch('/api/websites', { params: { user_id: userId } });
|
||||
|
||||
if (!data) {
|
||||
return null;
|
||||
|
@ -17,9 +17,9 @@ export default function WebsiteList({ userId }) {
|
|||
|
||||
return (
|
||||
<Page>
|
||||
{data.map(({ website_id, name }) => (
|
||||
{data.map(({ website_id, name, domain }) => (
|
||||
<div key={website_id} className={styles.website}>
|
||||
<WebsiteChart websiteId={website_id} title={name} showLink />
|
||||
<WebsiteChart websiteId={website_id} title={name} domain={domain} showLink />
|
||||
</div>
|
||||
))}
|
||||
{data.length === 0 && (
|
||||
|
|
|
@ -25,7 +25,7 @@ export default function AccountSettings() {
|
|||
const [deleteAccount, setDeleteAccount] = useState();
|
||||
const [saved, setSaved] = useState(0);
|
||||
const [message, setMessage] = useState();
|
||||
const { data } = useFetch(`/api/accounts`, {}, { update: [saved] });
|
||||
const { data } = useFetch(`/api/accounts`, {}, [saved]);
|
||||
|
||||
const Checkmark = ({ is_admin }) => (is_admin ? <Icon icon={<Check />} size="medium" /> : null);
|
||||
|
||||
|
@ -42,10 +42,10 @@ export default function AccountSettings() {
|
|||
row.username !== 'admin' ? (
|
||||
<ButtonLayout align="right">
|
||||
<Button icon={<Pen />} size="small" onClick={() => setEditAccount(row)}>
|
||||
<FormattedMessage id="button.edit" defaultMessage="Edit" />
|
||||
<FormattedMessage id="label.edit" defaultMessage="Edit" />
|
||||
</Button>
|
||||
<Button icon={<Trash />} size="small" onClick={() => setDeleteAccount(row)}>
|
||||
<FormattedMessage id="button.delete" defaultMessage="Delete" />
|
||||
<FormattedMessage id="label.delete" defaultMessage="Delete" />
|
||||
</Button>
|
||||
</ButtonLayout>
|
||||
) : null;
|
||||
|
@ -98,12 +98,12 @@ export default function AccountSettings() {
|
|||
<FormattedMessage id="label.accounts" defaultMessage="Accounts" />
|
||||
</div>
|
||||
<Button icon={<Plus />} size="small" onClick={() => setAddAccount(true)}>
|
||||
<FormattedMessage id="button.add-account" defaultMessage="Add account" />
|
||||
<FormattedMessage id="label.add-account" defaultMessage="Add account" />
|
||||
</Button>
|
||||
</PageHeader>
|
||||
<Table columns={columns} rows={data} />
|
||||
{editAccount && (
|
||||
<Modal title={<FormattedMessage id="title.edit-account" defaultMessage="Edit account" />}>
|
||||
<Modal title={<FormattedMessage id="label.edit-account" defaultMessage="Edit account" />}>
|
||||
<AccountEditForm
|
||||
values={{ ...editAccount, password: '' }}
|
||||
onSave={handleSave}
|
||||
|
@ -112,13 +112,13 @@ export default function AccountSettings() {
|
|||
</Modal>
|
||||
)}
|
||||
{addAccount && (
|
||||
<Modal title={<FormattedMessage id="title.add-account" defaultMessage="Add account" />}>
|
||||
<Modal title={<FormattedMessage id="label.add-account" defaultMessage="Add account" />}>
|
||||
<AccountEditForm onSave={handleSave} onClose={handleClose} />
|
||||
</Modal>
|
||||
)}
|
||||
{deleteAccount && (
|
||||
<Modal
|
||||
title={<FormattedMessage id="title.delete-account" defaultMessage="Delete account" />}
|
||||
title={<FormattedMessage id="label.delete-account" defaultMessage="Delete account" />}
|
||||
>
|
||||
<DeleteForm
|
||||
values={{ type: 'account', id: deleteAccount.user_id, name: deleteAccount.username }}
|
||||
|
|
|
@ -19,7 +19,7 @@ export default function DateRangeSetting() {
|
|||
<>
|
||||
<DateFilter value={value} startDate={startDate} endDate={endDate} onChange={setDateRange} />
|
||||
<Button className={styles.button} size="small" onClick={handleReset}>
|
||||
<FormattedMessage id="button.reset" defaultMessage="Reset" />
|
||||
<FormattedMessage id="label.reset" defaultMessage="Reset" />
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
|
|
|
@ -16,7 +16,7 @@ export default function LanguageButton() {
|
|||
return (
|
||||
<>
|
||||
<Head>
|
||||
{locale === 'zh-CN' && (
|
||||
{(locale === 'zh-CN' || locale === 'zh-TW') && (
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@400;500;700&display=swap"
|
||||
rel="stylesheet"
|
||||
|
|
|
@ -34,7 +34,7 @@ export default function ProfileSettings() {
|
|||
<FormattedMessage id="label.profile" defaultMessage="Profile" />
|
||||
</div>
|
||||
<Button icon={<Dots />} size="small" onClick={() => setChangePassword(true)}>
|
||||
<FormattedMessage id="button.change-password" defaultMessage="Change password" />
|
||||
<FormattedMessage id="label.change-password" defaultMessage="Change password" />
|
||||
</Button>
|
||||
</PageHeader>
|
||||
<dl className={styles.list}>
|
||||
|
@ -57,7 +57,7 @@ export default function ProfileSettings() {
|
|||
</dl>
|
||||
{changePassword && (
|
||||
<Modal
|
||||
title={<FormattedMessage id="title.change-password" defaultMessage="Change password" />}
|
||||
title={<FormattedMessage id="label.change-password" defaultMessage="Change password" />}
|
||||
>
|
||||
<ChangePasswordForm
|
||||
values={{ user_id }}
|
||||
|
|
|
@ -24,7 +24,7 @@ export default function TimezoneSetting() {
|
|||
onChange={saveTimezone}
|
||||
/>
|
||||
<Button className={styles.button} size="small" onClick={handleReset}>
|
||||
<FormattedMessage id="button.reset" defaultMessage="Reset" />
|
||||
<FormattedMessage id="label.reset" defaultMessage="Reset" />
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
|
|
|
@ -13,6 +13,7 @@ import ShareUrlForm from 'components/forms/ShareUrlForm';
|
|||
import EmptyPlaceholder from 'components/common/EmptyPlaceholder';
|
||||
import ButtonLayout from 'components/layout/ButtonLayout';
|
||||
import Toast from 'components/common/Toast';
|
||||
import Favicon from 'components/common/Favicon';
|
||||
import Pen from 'assets/pen.svg';
|
||||
import Trash from 'assets/trash.svg';
|
||||
import Plus from 'assets/plus.svg';
|
||||
|
@ -29,7 +30,7 @@ export default function WebsiteSettings() {
|
|||
const [showUrl, setShowUrl] = useState();
|
||||
const [saved, setSaved] = useState(0);
|
||||
const [message, setMessage] = useState();
|
||||
const { data } = useFetch(`/api/websites`, {}, { update: [saved] });
|
||||
const { data } = useFetch(`/api/websites`, {}, [saved]);
|
||||
|
||||
const Buttons = row => (
|
||||
<ButtonLayout align="right">
|
||||
|
@ -52,16 +53,17 @@ export default function WebsiteSettings() {
|
|||
onClick={() => setShowCode(row)}
|
||||
/>
|
||||
<Button icon={<Pen />} size="small" onClick={() => setEditWebsite(row)}>
|
||||
<FormattedMessage id="button.edit" defaultMessage="Edit" />
|
||||
<FormattedMessage id="label.edit" defaultMessage="Edit" />
|
||||
</Button>
|
||||
<Button icon={<Trash />} size="small" onClick={() => setDeleteWebsite(row)}>
|
||||
<FormattedMessage id="button.delete" defaultMessage="Delete" />
|
||||
<FormattedMessage id="label.delete" defaultMessage="Delete" />
|
||||
</Button>
|
||||
</ButtonLayout>
|
||||
);
|
||||
|
||||
const DetailsLink = ({ website_id, name }) => (
|
||||
const DetailsLink = ({ website_id, name, domain }) => (
|
||||
<Link href="/website/[...id]" as={`/website/${website_id}/${name}`}>
|
||||
<Favicon domain={domain} />
|
||||
{name}
|
||||
</Link>
|
||||
);
|
||||
|
@ -113,7 +115,7 @@ export default function WebsiteSettings() {
|
|||
}
|
||||
>
|
||||
<Button icon={<Plus />} size="medium" onClick={() => setAddWebsite(true)}>
|
||||
<FormattedMessage id="button.add-website" defaultMessage="Add website" />
|
||||
<FormattedMessage id="label.add-website" defaultMessage="Add website" />
|
||||
</Button>
|
||||
</EmptyPlaceholder>
|
||||
);
|
||||
|
@ -125,23 +127,23 @@ export default function WebsiteSettings() {
|
|||
<FormattedMessage id="label.websites" defaultMessage="Websites" />
|
||||
</div>
|
||||
<Button icon={<Plus />} size="small" onClick={() => setAddWebsite(true)}>
|
||||
<FormattedMessage id="button.add-website" defaultMessage="Add website" />
|
||||
<FormattedMessage id="label.add-website" defaultMessage="Add website" />
|
||||
</Button>
|
||||
</PageHeader>
|
||||
<Table columns={columns} rows={data} empty={empty} />
|
||||
{editWebsite && (
|
||||
<Modal title={<FormattedMessage id="title.edit-website" defaultMessage="Edit website" />}>
|
||||
<Modal title={<FormattedMessage id="label.edit-website" defaultMessage="Edit website" />}>
|
||||
<WebsiteEditForm values={editWebsite} onSave={handleSave} onClose={handleClose} />
|
||||
</Modal>
|
||||
)}
|
||||
{addWebsite && (
|
||||
<Modal title={<FormattedMessage id="title.add-website" defaultMessage="Add website" />}>
|
||||
<Modal title={<FormattedMessage id="label.add-website" defaultMessage="Add website" />}>
|
||||
<WebsiteEditForm onSave={handleSave} onClose={handleClose} />
|
||||
</Modal>
|
||||
)}
|
||||
{deleteWebsite && (
|
||||
<Modal
|
||||
title={<FormattedMessage id="title.delete-website" defaultMessage="Delete website" />}
|
||||
title={<FormattedMessage id="label.delete-website" defaultMessage="Delete website" />}
|
||||
>
|
||||
<DeleteForm
|
||||
values={{ type: 'website', id: deleteWebsite.website_id, name: deleteWebsite.name }}
|
||||
|
@ -151,12 +153,12 @@ export default function WebsiteSettings() {
|
|||
</Modal>
|
||||
)}
|
||||
{showCode && (
|
||||
<Modal title={<FormattedMessage id="title.tracking-code" defaultMessage="Tracking code" />}>
|
||||
<Modal title={<FormattedMessage id="label.tracking-code" defaultMessage="Tracking code" />}>
|
||||
<TrackingCodeForm values={showCode} onClose={handleClose} />
|
||||
</Modal>
|
||||
)}
|
||||
{showUrl && (
|
||||
<Modal title={<FormattedMessage id="title.share-url" defaultMessage="Share URL" />}>
|
||||
<Modal title={<FormattedMessage id="label.share-url" defaultMessage="Share URL" />}>
|
||||
<ShareUrlForm values={showUrl} onClose={handleClose} />
|
||||
</Modal>
|
||||
)}
|
||||
|
|
|
@ -4,24 +4,22 @@ import { get } from 'lib/web';
|
|||
import { updateQuery } from 'redux/actions/queries';
|
||||
import { useRouter } from 'next/router';
|
||||
|
||||
export default function useFetch(url, params = {}, options = {}) {
|
||||
export default function useFetch(url, options = {}, update = []) {
|
||||
const dispatch = useDispatch();
|
||||
const [data, setData] = useState();
|
||||
const [status, setStatus] = useState();
|
||||
const [error, setError] = useState();
|
||||
const [loading, setLoadiing] = useState(false);
|
||||
const [count, setCount] = useState(0);
|
||||
const { basePath } = useRouter();
|
||||
const keys = Object.keys(params)
|
||||
.sort()
|
||||
.map(key => params[key]);
|
||||
const { update = [], onDataLoad = () => {} } = options;
|
||||
const { params = {}, disabled, headers, delay = 0, interval, onDataLoad } = options;
|
||||
|
||||
async function loadData() {
|
||||
async function loadData(params) {
|
||||
try {
|
||||
setLoadiing(true);
|
||||
setError(null);
|
||||
const time = performance.now();
|
||||
const { data, status } = await get(`${basePath}${url}`, params);
|
||||
const { data, status } = await get(`${basePath}${url}`, params, headers);
|
||||
|
||||
dispatch(updateQuery({ url, time: performance.now() - time, completed: Date.now() }));
|
||||
|
||||
|
@ -33,7 +31,7 @@ export default function useFetch(url, params = {}, options = {}) {
|
|||
}
|
||||
|
||||
setStatus(status);
|
||||
onDataLoad(data);
|
||||
onDataLoad?.(data);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
setError(e);
|
||||
|
@ -43,18 +41,24 @@ export default function useFetch(url, params = {}, options = {}) {
|
|||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (url) {
|
||||
const { interval, delay = 0 } = options;
|
||||
if (url && !disabled) {
|
||||
const id = setTimeout(() => loadData(params), delay);
|
||||
|
||||
setTimeout(() => loadData(), delay);
|
||||
return () => {
|
||||
clearTimeout(id);
|
||||
};
|
||||
}
|
||||
}, [url, !!disabled, count, ...update]);
|
||||
|
||||
const id = interval ? setInterval(() => loadData(), interval) : null;
|
||||
useEffect(() => {
|
||||
if (interval && !disabled) {
|
||||
const id = setInterval(() => setCount(state => state + 1), interval);
|
||||
|
||||
return () => {
|
||||
clearInterval(id);
|
||||
};
|
||||
}
|
||||
}, [url, ...keys, ...update]);
|
||||
}, [interval, !!disabled]);
|
||||
|
||||
return { data, status, error, loading };
|
||||
}
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
import { useEffect } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { get } from 'lib/web';
|
||||
import { setShareToken } from 'redux/actions/app';
|
||||
|
||||
export default function useShareToken(shareId) {
|
||||
const dispatch = useDispatch();
|
||||
const shareToken = useSelector(state => state.app.shareToken);
|
||||
|
||||
async function loadToken(id) {
|
||||
const { data } = await get(`/api/share/${id}`);
|
||||
|
||||
if (data) {
|
||||
dispatch(setShareToken(data));
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (shareId) {
|
||||
loadToken(shareId);
|
||||
}
|
||||
}, [shareId]);
|
||||
|
||||
return shareToken;
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
{
|
||||
"cs-CZ": ["label.reset", "metrics.device.tablet"],
|
||||
"de-DE": [
|
||||
"label.administrator",
|
||||
"label.name",
|
||||
"label.domain",
|
||||
"metrics.device.desktop",
|
||||
"metrics.device.laptop",
|
||||
"metrics.device.tablet",
|
||||
"metrics.referrers"
|
||||
],
|
||||
"fr-FR": ["metrics.actions", "metrics.pages"],
|
||||
"nb-NO": ["label.administrator", "label.dashboard"],
|
||||
"nl-NL": [
|
||||
"label.administrator",
|
||||
"label.websites",
|
||||
"metrics.browsers",
|
||||
"metrics.device.desktop",
|
||||
"metrics.device.laptop",
|
||||
"metrics.device.tablet"
|
||||
]
|
||||
}
|
|
@ -0,0 +1,99 @@
|
|||
{
|
||||
"label.accounts": "Účty",
|
||||
"label.add-account": "Přidat účet",
|
||||
"label.add-website": "Přidat web",
|
||||
"label.administrator": "Administrátor",
|
||||
"label.all": "Vše",
|
||||
"label.all-websites": "Všechny weby",
|
||||
"label.back": "Zpět",
|
||||
"label.cancel": "Zrušit",
|
||||
"label.change-password": "Změnit heslo",
|
||||
"label.confirm-password": "Potvrdit heslo",
|
||||
"label.copy-to-clipboard": "Kopírovat do schránky",
|
||||
"label.current-password": "Aktuální heslo",
|
||||
"label.custom-range": "Vlastní rozsah",
|
||||
"label.dashboard": "Přehled",
|
||||
"label.date-range": "Období",
|
||||
"label.default-date-range": "Výchozí období",
|
||||
"label.delete": "Smazat",
|
||||
"label.delete-account": "Smazat účet",
|
||||
"label.delete-website": "Smazat web",
|
||||
"label.dismiss": "Odejít",
|
||||
"label.domain": "Doména",
|
||||
"label.edit": "Upravit",
|
||||
"label.edit-account": "Upravit účet",
|
||||
"label.edit-website": "Upravit web",
|
||||
"label.enable-share-url": "Povolit sdílení URL",
|
||||
"label.invalid": "Neplatný",
|
||||
"label.invalid-domain": "Neplatná doména",
|
||||
"label.last-days": "Posledních {x} dnů",
|
||||
"label.last-hours": "Posledních {x} hodin",
|
||||
"label.logged-in-as": "Přihlášený jako {username}",
|
||||
"label.login": "Přihlásit",
|
||||
"label.logout": "Odhlásit",
|
||||
"label.more": "Více",
|
||||
"label.name": "Jméno",
|
||||
"label.new-password": "Nové heslo",
|
||||
"label.password": "Heslo",
|
||||
"label.passwords-dont-match": "Hesla se neschodují",
|
||||
"label.profile": "Profil",
|
||||
"label.realtime": "Aktuálně",
|
||||
"label.realtime-logs": "Aktuální záznamy",
|
||||
"label.refresh": "Obnovit",
|
||||
"label.required": "Vyžadováno",
|
||||
"label.reset": "Reset",
|
||||
"label.save": "Uložit",
|
||||
"label.settings": "Nastavení",
|
||||
"label.share-url": "Sdílet URL",
|
||||
"label.single-day": "Jeden den",
|
||||
"label.this-month": "Tento měsíc",
|
||||
"label.this-week": "Tento týden",
|
||||
"label.this-year": "Tento rok",
|
||||
"label.timezone": "Časová zóna",
|
||||
"label.today": "Dnes",
|
||||
"label.tracking-code": "Sledovací kód",
|
||||
"label.unknown": "Neznámý",
|
||||
"label.username": "Uživatelské jméno",
|
||||
"label.view-details": "Zobrazit detaily",
|
||||
"label.websites": "Weby",
|
||||
"message.active-users": "{x} aktuálně {x, plural, one {návštěvník} other {návštěvníci}}",
|
||||
"message.confirm-delete": "Opravdu smazat {target}?",
|
||||
"message.copied": "Zkopírováno!",
|
||||
"message.delete-warning": "Všechna související data budou také smazána.",
|
||||
"message.failure": "Něco se pokazilo.",
|
||||
"message.get-share-url": "Získat sdílené URL",
|
||||
"message.get-tracking-code": "Získat měřící kód",
|
||||
"message.go-to-settings": "Jít do nastavení",
|
||||
"message.incorrect-username-password": "Nesprávné jméno/heslo.",
|
||||
"message.log.visitor": "Návštěvník z {country} s prohlížečem {browser} na {os} {device}",
|
||||
"message.new-version-available": "Nová verze umami {version} je k dispozici!",
|
||||
"message.no-data-available": "Žádná data.",
|
||||
"message.no-websites-configured": "Nemáte nastavený žádný web.",
|
||||
"message.page-not-found": "Stránka nenalezena.",
|
||||
"message.powered-by": "Běží na {name}",
|
||||
"message.save-success": "Úspěšně uloženo.",
|
||||
"message.share-url": "Toto je sdílené URL pro {target}.",
|
||||
"message.track-stats": "Pro sledování návštěv na {target}, přidejte následující kód do {head} části vašeho webu.",
|
||||
"message.type-delete": "Napište {delete} pro potvrzení.",
|
||||
"metrics.actions": "Akce",
|
||||
"metrics.average-visit-time": "Průměrný čas návštěvy",
|
||||
"metrics.bounce-rate": "Okamžité opuštění",
|
||||
"metrics.browsers": "Prohlížeč",
|
||||
"metrics.countries": "Země",
|
||||
"metrics.device.desktop": "Stolní počítač",
|
||||
"metrics.device.laptop": "Přenosný počítač",
|
||||
"metrics.device.mobile": "Mobilní telefon",
|
||||
"metrics.device.tablet": "Tablet",
|
||||
"metrics.devices": "Zařízení",
|
||||
"metrics.events": "Události",
|
||||
"metrics.filter.combined": "Kombinace",
|
||||
"metrics.filter.domain-only": "Domény",
|
||||
"metrics.filter.raw": "Nezpracované",
|
||||
"metrics.operating-systems": "Operační systém",
|
||||
"metrics.page-views": "Zobrazení stránek",
|
||||
"metrics.pages": "Stránky",
|
||||
"metrics.referrers": "Odkazy",
|
||||
"metrics.unique-visitors": "Jedinečné návštěvy",
|
||||
"metrics.views": "Zobrazení",
|
||||
"metrics.visitors": "Návštěvy"
|
||||
}
|
|
@ -1,50 +1,60 @@
|
|||
{
|
||||
"button.add-account": "Tilføj konto",
|
||||
"button.add-website": "Tilføj hjemmeside",
|
||||
"button.back": "Tilbage",
|
||||
"button.cancel": "Afvis",
|
||||
"button.change-password": "Skift adgangskode",
|
||||
"button.copy-to-clipboard": "Kopier til udklipsholder",
|
||||
"button.date-range": "Datointerval",
|
||||
"button.delete": "Slet",
|
||||
"button.dismiss": "Dismiss",
|
||||
"button.edit": "Rediger",
|
||||
"button.login": "Log ind",
|
||||
"button.more": "Mere",
|
||||
"button.refresh": "Opdater",
|
||||
"button.reset": "Reset",
|
||||
"button.save": "Gem",
|
||||
"button.single-day": "Enkelt dag",
|
||||
"button.view-details": "Vis detajler",
|
||||
"label.accounts": "Kontoer",
|
||||
"label.add-account": "Tilføj konto",
|
||||
"label.add-website": "Tilføj hjemmeside",
|
||||
"label.administrator": "Administrator",
|
||||
"label.all": "All",
|
||||
"label.all-websites": "All websites",
|
||||
"label.back": "Tilbage",
|
||||
"label.cancel": "Afvis",
|
||||
"label.change-password": "Skift adgangskode",
|
||||
"label.confirm-password": "Godkendt adgangskode",
|
||||
"label.copy-to-clipboard": "Kopier til udklipsholder",
|
||||
"label.current-password": "Nuværende adgangskode",
|
||||
"label.custom-range": "Tilpasset interval",
|
||||
"label.dashboard": "Betjeningspanel",
|
||||
"label.date-range": "Datointerval",
|
||||
"label.default-date-range": "Default date range",
|
||||
"label.delete": "Slet",
|
||||
"label.delete-account": "Slet konto",
|
||||
"label.delete-website": "Slet hjemmeside",
|
||||
"label.dismiss": "Dismiss",
|
||||
"label.domain": "Domæne",
|
||||
"label.edit": "Rediger",
|
||||
"label.edit-account": "Rediger konto",
|
||||
"label.edit-website": "Rediger hjemmeside",
|
||||
"label.enable-share-url": "Aktivér delings-URL",
|
||||
"label.invalid": "Ugyldig",
|
||||
"label.invalid-domain": "Ugyldigt domæne",
|
||||
"label.last-days": "Sidste {x} dage",
|
||||
"label.last-hours": "Sidste {x} timer",
|
||||
"label.logged-in-as": "Loggede ind som {username}",
|
||||
"label.login": "Log ind",
|
||||
"label.logout": "Log ud",
|
||||
"label.more": "Mere",
|
||||
"label.name": "Navn",
|
||||
"label.new-password": "Ny adgangskode",
|
||||
"label.password": "Adgangskode",
|
||||
"label.passwords-dont-match": "Adgangskoder matcher ikke",
|
||||
"label.profile": "Profil",
|
||||
"label.realtime": "Realtime",
|
||||
"label.realtime-logs": "Realtime logs",
|
||||
"label.refresh": "Opdater",
|
||||
"label.required": "Påkrævet",
|
||||
"label.reset": "Reset",
|
||||
"label.save": "Gem",
|
||||
"label.settings": "Indstillinger",
|
||||
"label.share-url": "Del URL",
|
||||
"label.single-day": "Enkelt dag",
|
||||
"label.this-month": "Denne måned",
|
||||
"label.this-week": "Denne uge",
|
||||
"label.this-year": "Dette år",
|
||||
"label.timezone": "Timezone",
|
||||
"label.today": "Idag",
|
||||
"label.tracking-code": "Sporingskode",
|
||||
"label.unknown": "Ukendt",
|
||||
"label.username": "Brugernavn",
|
||||
"label.view-details": "Vis detajler",
|
||||
"label.websites": "Hjemmesider",
|
||||
"message.active-users": "{x} nuværende {x, plural, one {bruger} other {brugere}}",
|
||||
"message.confirm-delete": "Er du sikker på at du vil slette {target}?",
|
||||
|
@ -55,6 +65,7 @@
|
|||
"message.get-tracking-code": "Få sporingskode",
|
||||
"message.go-to-settings": "Gå til betjeningspanel",
|
||||
"message.incorrect-username-password": "Ugyldigt brugernavn/adgangskode.",
|
||||
"message.log.visitor": "Visitor from {country} using {browser} on {os} {device}",
|
||||
"message.new-version-available": "A new version of umami {version} is available!",
|
||||
"message.no-data-available": "Ingen data tilgængelig.",
|
||||
"message.no-websites-configured": "Du har ikke konfigureret nogen websteder.",
|
||||
|
@ -84,14 +95,5 @@
|
|||
"metrics.referrers": "Henvisninger",
|
||||
"metrics.unique-visitors": "Unikke besøgende",
|
||||
"metrics.views": "Visninger",
|
||||
"metrics.visitors": "Besøgende",
|
||||
"title.add-account": "Tilføj konto",
|
||||
"title.add-website": "Tilføj hjemmeside",
|
||||
"title.change-password": "Skift adgangskode",
|
||||
"title.delete-account": "Slet konto",
|
||||
"title.delete-website": "Slet hjemmeside",
|
||||
"title.edit-account": "Rediger konto",
|
||||
"title.edit-website": "Rediger hjemmeside",
|
||||
"title.share-url": "Del URL",
|
||||
"title.tracking-code": "Sporingskode"
|
||||
"metrics.visitors": "Besøgende"
|
||||
}
|
||||
|
|
|
@ -1,50 +1,60 @@
|
|||
{
|
||||
"button.add-account": "Konto hinzufügen",
|
||||
"button.add-website": "Webseite hinzufügen",
|
||||
"button.back": "Zurück",
|
||||
"button.cancel": "Abbrechen",
|
||||
"button.change-password": "Passwort ändern",
|
||||
"button.copy-to-clipboard": "In die Zwischenablage kopieren",
|
||||
"button.date-range": "Datumsbereich",
|
||||
"button.delete": "Löschen",
|
||||
"button.dismiss": "Verwerfen",
|
||||
"button.edit": "Bearbeiten",
|
||||
"button.login": "Anmelden",
|
||||
"button.more": "Mehr",
|
||||
"button.refresh": "Aktualisieren",
|
||||
"button.reset": "Zurücksetzen",
|
||||
"button.save": "Speichern",
|
||||
"button.single-day": "Ein Tag",
|
||||
"button.view-details": "Details anzeigen",
|
||||
"label.accounts": "Konten",
|
||||
"label.add-account": "Konto hinzugfügen",
|
||||
"label.add-website": "Webseite hinzufügen",
|
||||
"label.administrator": "Administrator",
|
||||
"label.all": "Alle",
|
||||
"label.all-websites": "Alle Webseiten",
|
||||
"label.back": "Zurück",
|
||||
"label.cancel": "Abbrechen",
|
||||
"label.change-password": "Passwort ändern",
|
||||
"label.confirm-password": "Passwort wiederholen",
|
||||
"label.copy-to-clipboard": "In die Zwischenablage kopieren",
|
||||
"label.current-password": "Derzeitiges Passwort",
|
||||
"label.custom-range": "Benutzerdefinierter Bereich",
|
||||
"label.dashboard": "Übersicht",
|
||||
"label.date-range": "Datumsbereich",
|
||||
"label.default-date-range": "Voreingestellter Datumsbereich",
|
||||
"label.delete": "Löschen",
|
||||
"label.delete-account": "Konto löschen",
|
||||
"label.delete-website": "Webseite löschen",
|
||||
"label.dismiss": "Verwerfen",
|
||||
"label.domain": "Domain",
|
||||
"label.edit": "Bearbeiten",
|
||||
"label.edit-account": "Konto bearbeiten",
|
||||
"label.edit-website": "Webseite bearbeiten",
|
||||
"label.enable-share-url": "Freigabe-URL aktivieren",
|
||||
"label.invalid": "Ungültig",
|
||||
"label.invalid-domain": "Ungültige Domain",
|
||||
"label.last-days": "Letzten {x} Tage",
|
||||
"label.last-hours": "Letzten {x} Stunden",
|
||||
"label.logged-in-as": "Angemeldet als {username}",
|
||||
"label.login": "Anmelden",
|
||||
"label.logout": "Abmelden",
|
||||
"label.more": "Mehr",
|
||||
"label.name": "Name",
|
||||
"label.new-password": "Neues Passwort",
|
||||
"label.password": "Passwort",
|
||||
"label.passwords-dont-match": "Passwörter stimmen nicht überein",
|
||||
"label.profile": "Profil",
|
||||
"label.realtime": "Echtzeit",
|
||||
"label.realtime-logs": "Echtzeit Logs",
|
||||
"label.refresh": "Aktualisieren",
|
||||
"label.required": "Erforderlich",
|
||||
"label.reset": "Zurücksetzen",
|
||||
"label.save": "Speichern",
|
||||
"label.settings": "Einstellungen",
|
||||
"label.share-url": "Freigabe-URL",
|
||||
"label.single-day": "Ein Tag",
|
||||
"label.this-month": "Diesen Monat",
|
||||
"label.this-week": "Diese Woche",
|
||||
"label.this-year": "Dieses Jahr",
|
||||
"label.timezone": "Zeitzone",
|
||||
"label.today": "Heute",
|
||||
"label.tracking-code": "Tracking Kennung",
|
||||
"label.unknown": "Unbekannt",
|
||||
"label.username": "Benutzername",
|
||||
"label.view-details": "Details anzeigen",
|
||||
"label.websites": "Webseiten",
|
||||
"message.active-users": "{x} {x, plural, one {aktiver Besucher} other {aktive Besucher}}",
|
||||
"message.confirm-delete": "Sind sie sich sicher {target} zu löschen?",
|
||||
|
@ -55,6 +65,7 @@
|
|||
"message.get-tracking-code": "Erstelle Tracking Kennung",
|
||||
"message.go-to-settings": "Zu den Einstellungen",
|
||||
"message.incorrect-username-password": "Falsches Passwort oder Benutzername.",
|
||||
"message.log.visitor": "Besucher aus {country} benutzt {browser} auf {os} {device}",
|
||||
"message.new-version-available": "Eine neue Version umami {version} ist verfügbar!",
|
||||
"message.no-data-available": "Keine Daten vorhanden.",
|
||||
"message.no-websites-configured": "Es ist keine Webseite vorhanden.",
|
||||
|
@ -67,7 +78,7 @@
|
|||
"metrics.actions": "Aktionen",
|
||||
"metrics.average-visit-time": "Durchschn. Besuchszeit",
|
||||
"metrics.bounce-rate": "Absprungrate",
|
||||
"metrics.browsers": "Browsers",
|
||||
"metrics.browsers": "Browser",
|
||||
"metrics.countries": "Länder",
|
||||
"metrics.device.desktop": "Desktop",
|
||||
"metrics.device.laptop": "Laptop",
|
||||
|
@ -84,14 +95,5 @@
|
|||
"metrics.referrers": "Referrers",
|
||||
"metrics.unique-visitors": "Eindeutige Besucher",
|
||||
"metrics.views": "Aufrufe",
|
||||
"metrics.visitors": "Besucher",
|
||||
"title.add-account": "Konto hinzugfügen",
|
||||
"title.add-website": "Webseite hinzufügen",
|
||||
"title.change-password": "Passwort ändern",
|
||||
"title.delete-account": "Konto löschen",
|
||||
"title.delete-website": "Webseite löschen",
|
||||
"title.edit-account": "Konto bearbeiten",
|
||||
"title.edit-website": "Webseite bearbeiten",
|
||||
"title.share-url": "Freigabe-URL",
|
||||
"title.tracking-code": "Tracking Kennung"
|
||||
"metrics.visitors": "Besucher"
|
||||
}
|
||||
|
|
|
@ -1,50 +1,60 @@
|
|||
{
|
||||
"button.add-account": "Προσθήκη λογαριασμού",
|
||||
"button.add-website": "Προσθήκη ιστότοπου",
|
||||
"button.back": "Πίσω",
|
||||
"button.cancel": "Ακύρωση",
|
||||
"button.change-password": "Αλλαγή κωδικού",
|
||||
"button.copy-to-clipboard": "Αντιγραφή στο πρόχειρο",
|
||||
"button.date-range": "Εύρος ημερομηνιών",
|
||||
"button.delete": "Διαγραφή",
|
||||
"button.dismiss": "Dismiss",
|
||||
"button.edit": "Επεξεργασία",
|
||||
"button.login": "Είσοδος",
|
||||
"button.more": "Περισσότερα",
|
||||
"button.refresh": "Ανανέωση",
|
||||
"button.reset": "Επαναφορά",
|
||||
"button.save": "Αποθήκευση",
|
||||
"button.single-day": "Ημερήσια",
|
||||
"button.view-details": "Λεπτομέρειες",
|
||||
"label.accounts": "Λογαριασμοί",
|
||||
"label.add-account": "Προσθήκη λογαριασμού",
|
||||
"label.add-website": "Προσθήκη ιστότοπου",
|
||||
"label.administrator": "Διαχειριστής",
|
||||
"label.all": "All",
|
||||
"label.all-websites": "All websites",
|
||||
"label.back": "Πίσω",
|
||||
"label.cancel": "Ακύρωση",
|
||||
"label.change-password": "Αλλαγή κωδικού",
|
||||
"label.confirm-password": "Επιβεβαίωση κωδικού",
|
||||
"label.copy-to-clipboard": "Αντιγραφή στο πρόχειρο",
|
||||
"label.current-password": "Τωρινός κωδικός πρόσβασης",
|
||||
"label.custom-range": "Προσαρμοσμένο εύρος",
|
||||
"label.dashboard": "Πίνακας",
|
||||
"label.date-range": "Εύρος ημερομηνιών",
|
||||
"label.default-date-range": "Προεπιλεγμένο εύρος ημερομηνιών",
|
||||
"label.delete": "Διαγραφή",
|
||||
"label.delete-account": "Διαγραφή λογαριασμού",
|
||||
"label.delete-website": "Διαγραφή ιστότοπου",
|
||||
"label.dismiss": "Dismiss",
|
||||
"label.domain": "Τομέας",
|
||||
"label.edit": "Επεξεργασία",
|
||||
"label.edit-account": "Επεξεργασία λογαριασμού",
|
||||
"label.edit-website": "Επεξεργασία ιστότοπου",
|
||||
"label.enable-share-url": "Ενεργοποίηση κοινής χρήσης URL",
|
||||
"label.invalid": "Μη έγκυρο",
|
||||
"label.invalid-domain": "Μη έγκυρος τομέας",
|
||||
"label.last-days": "Τελευταίες {x} ημέρες",
|
||||
"label.last-hours": "Τελευταίες {x} ώρες",
|
||||
"label.logged-in-as": "Συνδεθήκατε ως {username}",
|
||||
"label.login": "Είσοδος",
|
||||
"label.logout": "Αποσύνδεση",
|
||||
"label.more": "Περισσότερα",
|
||||
"label.name": "Όνομα",
|
||||
"label.new-password": "Νέος κωδικός",
|
||||
"label.password": "Κωδικός",
|
||||
"label.passwords-dont-match": "Οι κωδικοί πρόσβασης δεν ταιριάζουν",
|
||||
"label.profile": "Προφίλ",
|
||||
"label.realtime": "Realtime",
|
||||
"label.realtime-logs": "Realtime logs",
|
||||
"label.refresh": "Ανανέωση",
|
||||
"label.required": "Απαιτείται",
|
||||
"label.reset": "Επαναφορά",
|
||||
"label.save": "Αποθήκευση",
|
||||
"label.settings": "Ρυθμίσεις",
|
||||
"label.share-url": "Κοινοποίηση διεύθυνσης URL",
|
||||
"label.single-day": "Ημερήσια",
|
||||
"label.this-month": "Αυτο το μήνα",
|
||||
"label.this-week": "Αυτή την εβδομάδα",
|
||||
"label.this-year": "Αυτή την χρονιά",
|
||||
"label.timezone": "Ζώνη ώρας",
|
||||
"label.today": "Σήμερα",
|
||||
"label.tracking-code": "Κωδικός παρακολούθησης",
|
||||
"label.unknown": "Άγνωστο",
|
||||
"label.username": "Όνομα χρήστη",
|
||||
"label.view-details": "Λεπτομέρειες",
|
||||
"label.websites": "Ιστότοποι",
|
||||
"message.active-users": "{x} ενεργοί {x, plural, one {επισκέπτης} other {επισκέπτες}}",
|
||||
"message.confirm-delete": "Είστε βέβαιοι ότι θέλετε να διαγράψετε το {target};",
|
||||
|
@ -55,6 +65,7 @@
|
|||
"message.get-tracking-code": "Λήψη κώδικα παρακολούθησης",
|
||||
"message.go-to-settings": "Μεταβείτε στις ρυθμίσεις",
|
||||
"message.incorrect-username-password": "Εσφαλμένο όνομα χρήστη / κωδικός πρόσβασης.",
|
||||
"message.log.visitor": "Visitor from {country} using {browser} on {os} {device}",
|
||||
"message.new-version-available": "A new version of umami {version} is available!",
|
||||
"message.no-data-available": "Δεν υπάρχουν διαθέσιμα δεδομένα.",
|
||||
"message.no-websites-configured": "Δεν έχετε ρυθμίσει κανένα ιστότοπο.",
|
||||
|
@ -84,14 +95,5 @@
|
|||
"metrics.referrers": "Παραπομπές",
|
||||
"metrics.unique-visitors": "Μοναδικοί επισκέπτες",
|
||||
"metrics.views": "Προβολές",
|
||||
"metrics.visitors": "Επισκέπτες",
|
||||
"title.add-account": "Προσθήκη λογαριασμού",
|
||||
"title.add-website": "Προσθήκη ιστότοπου",
|
||||
"title.change-password": "Αλλαγή κωδικού",
|
||||
"title.delete-account": "Διαγραφή λογαριασμού",
|
||||
"title.delete-website": "Διαγραφή ιστότοπου",
|
||||
"title.edit-account": "Επεξεργασία λογαριασμού",
|
||||
"title.edit-website": "Επεξεργασία ιστότοπου",
|
||||
"title.share-url": "Κοινοποίηση διεύθυνσης URL",
|
||||
"title.tracking-code": "Κωδικός παρακολούθησης"
|
||||
"metrics.visitors": "Επισκέπτες"
|
||||
}
|
||||
|
|
|
@ -1,50 +1,60 @@
|
|||
{
|
||||
"button.add-account": "Add account",
|
||||
"button.add-website": "Add website",
|
||||
"button.back": "Back",
|
||||
"button.cancel": "Cancel",
|
||||
"button.change-password": "Change password",
|
||||
"button.copy-to-clipboard": "Copy to clipboard",
|
||||
"button.date-range": "Date range",
|
||||
"button.delete": "Delete",
|
||||
"button.dismiss": "Dismiss",
|
||||
"button.edit": "Edit",
|
||||
"button.login": "Login",
|
||||
"button.more": "More",
|
||||
"button.refresh": "Refresh",
|
||||
"button.reset": "Reset",
|
||||
"button.save": "Save",
|
||||
"button.single-day": "Single day",
|
||||
"button.view-details": "View details",
|
||||
"label.accounts": "Accounts",
|
||||
"label.add-account": "Add account",
|
||||
"label.add-website": "Add website",
|
||||
"label.administrator": "Administrator",
|
||||
"label.all": "All",
|
||||
"label.all-websites": "All websites",
|
||||
"label.back": "Back",
|
||||
"label.cancel": "Cancel",
|
||||
"label.change-password": "Change password",
|
||||
"label.confirm-password": "Confirm password",
|
||||
"label.copy-to-clipboard": "Copy to clipboard",
|
||||
"label.current-password": "Current password",
|
||||
"label.custom-range": "Custom range",
|
||||
"label.dashboard": "Dashboard",
|
||||
"label.date-range": "Date range",
|
||||
"label.default-date-range": "Default date range",
|
||||
"label.delete": "Delete",
|
||||
"label.delete-account": "Delete account",
|
||||
"label.delete-website": "Delete website",
|
||||
"label.dismiss": "Dismiss",
|
||||
"label.domain": "Domain",
|
||||
"label.edit": "Edit",
|
||||
"label.edit-account": "Edit account",
|
||||
"label.edit-website": "Edit website",
|
||||
"label.enable-share-url": "Enable share URL",
|
||||
"label.invalid": "Invalid",
|
||||
"label.invalid-domain": "Invalid domain",
|
||||
"label.last-days": "Last {x} days",
|
||||
"label.last-hours": "Last {x} hours",
|
||||
"label.logged-in-as": "Logged in as {username}",
|
||||
"label.login": "Login",
|
||||
"label.logout": "Logout",
|
||||
"label.more": "More",
|
||||
"label.name": "Name",
|
||||
"label.new-password": "New password",
|
||||
"label.password": "Password",
|
||||
"label.passwords-dont-match": "Passwords don't match",
|
||||
"label.profile": "Profile",
|
||||
"label.realtime": "Realtime",
|
||||
"label.realtime-logs": "Realtime logs",
|
||||
"label.refresh": "Refresh",
|
||||
"label.required": "Required",
|
||||
"label.reset": "Reset",
|
||||
"label.save": "Save",
|
||||
"label.settings": "Settings",
|
||||
"label.share-url": "Share URL",
|
||||
"label.single-day": "Single day",
|
||||
"label.this-month": "This month",
|
||||
"label.this-week": "This week",
|
||||
"label.this-year": "This year",
|
||||
"label.timezone": "Timezone",
|
||||
"label.today": "Today",
|
||||
"label.tracking-code": "Tracking code",
|
||||
"label.unknown": "Unknown",
|
||||
"label.username": "Username",
|
||||
"label.view-details": "View details",
|
||||
"label.websites": "Websites",
|
||||
"message.active-users": "{x} current {x, plural, one {visitor} other {visitors}}",
|
||||
"message.confirm-delete": "Are your sure you want to delete {target}?",
|
||||
|
@ -55,6 +65,7 @@
|
|||
"message.get-tracking-code": "Get tracking code",
|
||||
"message.go-to-settings": "Go to settings",
|
||||
"message.incorrect-username-password": "Incorrect username/password.",
|
||||
"message.log.visitor": "Visitor from {country} using {browser} on {os} {device}",
|
||||
"message.new-version-available": "A new version of umami {version} is available!",
|
||||
"message.no-data-available": "No data available.",
|
||||
"message.no-websites-configured": "You don't have any websites configured.",
|
||||
|
@ -84,14 +95,5 @@
|
|||
"metrics.referrers": "Referrers",
|
||||
"metrics.unique-visitors": "Unique visitors",
|
||||
"metrics.views": "Views",
|
||||
"metrics.visitors": "Visitors",
|
||||
"title.add-account": "Add account",
|
||||
"title.add-website": "Add website",
|
||||
"title.change-password": "Change password",
|
||||
"title.delete-account": "Delete account",
|
||||
"title.delete-website": "Delete website",
|
||||
"title.edit-account": "Edit account",
|
||||
"title.edit-website": "Edit website",
|
||||
"title.share-url": "Share URL",
|
||||
"title.tracking-code": "Tracking code"
|
||||
"metrics.visitors": "Visitors"
|
||||
}
|
||||
|
|
|
@ -1,50 +1,60 @@
|
|||
{
|
||||
"button.add-account": "Agregar usuario",
|
||||
"button.add-website": "Agregar sitio",
|
||||
"button.back": "Atrás",
|
||||
"button.cancel": "Cancelar",
|
||||
"button.change-password": "Cambiar contraseña",
|
||||
"button.copy-to-clipboard": "Copiar al portapapeles",
|
||||
"button.date-range": "Date range",
|
||||
"button.delete": "Eliminar",
|
||||
"button.dismiss": "Dismiss",
|
||||
"button.edit": "Editar",
|
||||
"button.login": "Iniciar sesión",
|
||||
"button.more": "Más",
|
||||
"button.refresh": "Refresh",
|
||||
"button.reset": "Reset",
|
||||
"button.save": "Guardar",
|
||||
"button.single-day": "Single day",
|
||||
"button.view-details": "Ver detalles",
|
||||
"label.accounts": "Usuarios",
|
||||
"label.add-account": "Agregar usuario",
|
||||
"label.add-website": "Agregar sitio",
|
||||
"label.administrator": "Administrador",
|
||||
"label.all": "Todos",
|
||||
"label.all-websites": "Todos los sitios",
|
||||
"label.back": "Atrás",
|
||||
"label.cancel": "Cancelar",
|
||||
"label.change-password": "Cambiar contraseña",
|
||||
"label.confirm-password": "Confirmar contraseña",
|
||||
"label.copy-to-clipboard": "Copiar al portapapeles",
|
||||
"label.current-password": "Contraseña actual",
|
||||
"label.custom-range": "Custom range",
|
||||
"label.custom-range": "Intervalo personalizado",
|
||||
"label.dashboard": "Panel de control",
|
||||
"label.default-date-range": "Default date range",
|
||||
"label.date-range": "Fechas",
|
||||
"label.default-date-range": "Intervalo por defecto",
|
||||
"label.delete": "Eliminar",
|
||||
"label.delete-account": "Eliminar usuario",
|
||||
"label.delete-website": "Eliminar sitio",
|
||||
"label.dismiss": "Ignorar",
|
||||
"label.domain": "Dominio",
|
||||
"label.edit": "Editar",
|
||||
"label.edit-account": "Editar usuario",
|
||||
"label.edit-website": "Editar sitio",
|
||||
"label.enable-share-url": "Habilitar compartir URL",
|
||||
"label.invalid": "Inválido",
|
||||
"label.invalid-domain": "Dominio inválido",
|
||||
"label.last-days": "Últimos {x} días",
|
||||
"label.last-hours": "Últimas {x} horas",
|
||||
"label.logged-in-as": "Sesión iniciada como {username}",
|
||||
"label.login": "Iniciar sesión",
|
||||
"label.logout": "Cerrar sesión",
|
||||
"label.more": "Más",
|
||||
"label.name": "Nombre",
|
||||
"label.new-password": "Nueva contraseña",
|
||||
"label.password": "Contraseña",
|
||||
"label.passwords-dont-match": "Las contraseñas no coinciden",
|
||||
"label.profile": "Perfil",
|
||||
"label.realtime": "Tiempo real",
|
||||
"label.realtime-logs": "Registros en tiempo real",
|
||||
"label.refresh": "Actualizar",
|
||||
"label.required": "Requerido",
|
||||
"label.reset": "Reiniciar",
|
||||
"label.save": "Guardar",
|
||||
"label.settings": "Configuraciones",
|
||||
"label.share-url": "Compartir URL",
|
||||
"label.single-day": "Dia",
|
||||
"label.this-month": "Este mes",
|
||||
"label.this-week": "Esta semana",
|
||||
"label.this-year": "Este año",
|
||||
"label.timezone": "Timezone",
|
||||
"label.timezone": "Zona horaria",
|
||||
"label.today": "Hoy",
|
||||
"label.unknown": "Unknown",
|
||||
"label.tracking-code": "Código de rastreo",
|
||||
"label.unknown": "Desconocida",
|
||||
"label.username": "Nombre de usuario",
|
||||
"label.view-details": "Ver detalles",
|
||||
"label.websites": "Sitios",
|
||||
"message.active-users": "{x} {x, plural, one {activo} other {activos}}",
|
||||
"message.confirm-delete": "¿Estás seguro(a) de querer eliminar {target}?",
|
||||
|
@ -55,7 +65,8 @@
|
|||
"message.get-tracking-code": "Obtener código de rastreo",
|
||||
"message.go-to-settings": "Ir a la configuración",
|
||||
"message.incorrect-username-password": "Nombre de usuario o contraseña incorrectos.",
|
||||
"message.new-version-available": "A new version of umami {version} is available!",
|
||||
"message.log.visitor": "Visitante desde {country} usando {browser} en {os} {device}",
|
||||
"message.new-version-available": "Una nueva versíon de umami {version} esta disponible!",
|
||||
"message.no-data-available": "Sin información disponible.",
|
||||
"message.no-websites-configured": "No tienes ningún sitio configurado.",
|
||||
"message.page-not-found": "Page not found",
|
||||
|
@ -72,7 +83,7 @@
|
|||
"metrics.device.desktop": "Desktop",
|
||||
"metrics.device.laptop": "Laptop",
|
||||
"metrics.device.mobile": "Mobile",
|
||||
"metrics.device.tablet": "Tablet",
|
||||
"metrics.device.tablet": "Tableta",
|
||||
"metrics.devices": "Dispositivos",
|
||||
"metrics.events": "Eventos",
|
||||
"metrics.filter.combined": "Combinado",
|
||||
|
@ -84,14 +95,5 @@
|
|||
"metrics.referrers": "Referentes",
|
||||
"metrics.unique-visitors": "Visitantes únicos",
|
||||
"metrics.views": "Vistas",
|
||||
"metrics.visitors": "Visitantes",
|
||||
"title.add-account": "Agregar usuario",
|
||||
"title.add-website": "Agregar sitio",
|
||||
"title.change-password": "Cambiar contraseña",
|
||||
"title.delete-account": "Eliminar usuario",
|
||||
"title.delete-website": "Eliminar sitio",
|
||||
"title.edit-account": "Editar usuario",
|
||||
"title.edit-website": "Editar sitio",
|
||||
"title.share-url": "Compartir URL",
|
||||
"title.tracking-code": "Código de rastreo"
|
||||
"metrics.visitors": "Visitantes"
|
||||
}
|
||||
|
|
|
@ -0,0 +1,99 @@
|
|||
{
|
||||
"label.accounts": "Tilit",
|
||||
"label.add-account": "Lisää tili",
|
||||
"label.add-website": "Lisää verkkosivu",
|
||||
"label.administrator": "Järjestelmänvalvoja",
|
||||
"label.all": "Kaikki",
|
||||
"label.all-websites": "Kaikki verkkosivut",
|
||||
"label.back": "Takaisin",
|
||||
"label.cancel": "Peruuta",
|
||||
"label.change-password": "Vaihda salasana",
|
||||
"label.confirm-password": "Vahvista salasana",
|
||||
"label.copy-to-clipboard": "Kopioi leikepöydälle",
|
||||
"label.current-password": "Nykyinen salasana",
|
||||
"label.custom-range": "Mukautettu jakso",
|
||||
"label.dashboard": "Ohjauspaneeli",
|
||||
"label.date-range": "Ajanjakso",
|
||||
"label.default-date-range": "Oletusajanjakso",
|
||||
"label.delete": "Poista",
|
||||
"label.delete-account": "Poista tili",
|
||||
"label.delete-website": "Poista verkkosivu",
|
||||
"label.dismiss": "Hylkää",
|
||||
"label.domain": "Verkkotunnus",
|
||||
"label.edit": "Muokkaa",
|
||||
"label.edit-account": "Muokkaa tiliä",
|
||||
"label.edit-website": "Muokkaa verkkosivua",
|
||||
"label.enable-share-url": "Ota jakamisen URL-osoite käyttöön",
|
||||
"label.invalid": "Virheellinen",
|
||||
"label.invalid-domain": "Virheellinen verkkotunnus",
|
||||
"label.last-days": "Viimeisimmät {x} päivät",
|
||||
"label.last-hours": "Viimeisimmät {x} tunnit",
|
||||
"label.logged-in-as": "Kirjautuneena sisään nimellä {username}",
|
||||
"label.login": "Kirjaudu sisään",
|
||||
"label.logout": "Kirjaudu ulos",
|
||||
"label.more": "Lisää",
|
||||
"label.name": "Nimi",
|
||||
"label.new-password": "Uusi salasana",
|
||||
"label.password": "Salasana",
|
||||
"label.passwords-dont-match": "Salasanat eivät täsmää",
|
||||
"label.profile": "Profiili",
|
||||
"label.realtime": "Reaaliaikainen",
|
||||
"label.realtime-logs": "Reaaliaikaiset lokit",
|
||||
"label.refresh": "Päivitä",
|
||||
"label.required": "Vaaditaan",
|
||||
"label.reset": "Nollaa",
|
||||
"label.save": "Tallenna",
|
||||
"label.settings": "Asetukset",
|
||||
"label.share-url": "Jaa URL",
|
||||
"label.single-day": "Yksi päivä",
|
||||
"label.this-month": "Tämä kuukausi",
|
||||
"label.this-week": "Tämä viikko",
|
||||
"label.this-year": "Tämä vuosi",
|
||||
"label.timezone": "Aikavyöhyke",
|
||||
"label.today": "Tänään",
|
||||
"label.tracking-code": "Seurantakoodi",
|
||||
"label.unknown": "Tuntematon",
|
||||
"label.username": "Käyttäjänimi",
|
||||
"label.view-details": "Katso tiedot",
|
||||
"label.websites": "Verkkosivut",
|
||||
"message.active-users": "{x} nykyinen {x, plural, yksi {visitor} muut {visitors}}",
|
||||
"message.confirm-delete": "Haluatko varmasti poistaa {target}?",
|
||||
"message.copied": "Kopioitu!",
|
||||
"message.delete-warning": "Kaikki siihen liittyvät tiedot poistetaan.",
|
||||
"message.failure": "Jotain meni väärin.",
|
||||
"message.get-share-url": "Hanki jakamisen URL-osoite",
|
||||
"message.get-tracking-code": "Hanki seurantakoodi",
|
||||
"message.go-to-settings": "Mene asetuksiin",
|
||||
"message.incorrect-username-password": "Väärä käyttäjänimi/salasana.",
|
||||
"message.log.visitor": "Vierailija maasta {country} käyttäen selainta {browser} {os}-laitteella: {device}",
|
||||
"message.new-version-available": "Uusi versio umamista {version} on käytettävissä!",
|
||||
"message.no-data-available": "Tietoja ei ole käytettävissä.",
|
||||
"message.no-websites-configured": "Sinulla ei ole määritettyjä verkkosivustoja.",
|
||||
"message.page-not-found": "Sivua ei löydetty.",
|
||||
"message.powered-by": "Voimanlähteenä {name}",
|
||||
"message.save-success": "Tallennettu onnistuneesti.",
|
||||
"message.share-url": "Tämä on julkisesti jaettu URL-osoitteelle {target}.",
|
||||
"message.track-stats": "Jos haluat seurata kohteen {target} tilastoja, aseta seuraava koodi verkkosivustosi {head} osioon.",
|
||||
"message.type-delete": "Kirjoita {delete} alla olevaan ruutuun vahvistaaksesi.",
|
||||
"metrics.actions": "Toiminnat",
|
||||
"metrics.average-visit-time": "Keskimääräinen vierailuaika",
|
||||
"metrics.bounce-rate": "Välitön poistuminen",
|
||||
"metrics.browsers": "Selaimet",
|
||||
"metrics.countries": "Maat",
|
||||
"metrics.device.desktop": "Pöytäkone",
|
||||
"metrics.device.laptop": "Kannettava tietokone",
|
||||
"metrics.device.mobile": "Mobiili",
|
||||
"metrics.device.tablet": "Tabletti",
|
||||
"metrics.devices": "Laitteet",
|
||||
"metrics.events": "Tapahtumat",
|
||||
"metrics.filter.combined": "Yhdistetty",
|
||||
"metrics.filter.domain-only": "Vain verkkotunnus",
|
||||
"metrics.filter.raw": "Käsittelemätön",
|
||||
"metrics.operating-systems": "Käyttöjärjestelmät",
|
||||
"metrics.page-views": "Sivun näyttökertoja",
|
||||
"metrics.pages": "Sivut",
|
||||
"metrics.referrers": "Viittaajat",
|
||||
"metrics.unique-visitors": "Uniikit vierailijat",
|
||||
"metrics.views": "Näyttökertoja",
|
||||
"metrics.visitors": "Vierailijat"
|
||||
}
|
|
@ -1,67 +1,78 @@
|
|||
{
|
||||
"button.add-account": "Ger brúkara",
|
||||
"button.add-website": "Legg heimasíðu til",
|
||||
"button.back": "Aftur",
|
||||
"button.cancel": "Strika",
|
||||
"button.change-password": "Broyt loyniorð",
|
||||
"button.copy-to-clipboard": "Kopier til clipboard",
|
||||
"button.date-range": "Vel dato",
|
||||
"button.delete": "Sletta",
|
||||
"button.dismiss": "Dismiss",
|
||||
"button.edit": "Ger broyting",
|
||||
"button.login": "Rita inn",
|
||||
"button.more": "Meira",
|
||||
"button.refresh": "Endurskapa",
|
||||
"button.reset": "Nulstilla",
|
||||
"button.save": "Goym",
|
||||
"button.single-day": "Einkultur dagur",
|
||||
"button.view-details": "Vís upplýsingar",
|
||||
"label.accounts": "Brúkarar",
|
||||
"label.administrator": "Administrator",
|
||||
"label.add-account": "Ger brúkara",
|
||||
"label.add-website": "Legg heimasíðu afturat",
|
||||
"label.administrator": "Fyrisitari",
|
||||
"label.all": "Alt",
|
||||
"label.all-websites": "Allar heimasíður",
|
||||
"label.back": "Aftur",
|
||||
"label.cancel": "Strika",
|
||||
"label.change-password": "Skift loyniorð",
|
||||
"label.confirm-password": "Vátta loyniorð",
|
||||
"label.copy-to-clipboard": "Avrita til setiðborð",
|
||||
"label.current-password": "Núverandi loyniorð",
|
||||
"label.custom-range": "Tillaga spenni",
|
||||
"label.dashboard": "Yvirlitsskíggi",
|
||||
"label.default-date-range": "Standard dato",
|
||||
"label.date-range": "Vel dato",
|
||||
"label.default-date-range": "Forsett dato",
|
||||
"label.delete": "Sletta",
|
||||
"label.delete-account": "Sletta brúkara",
|
||||
"label.delete-website": "Sletta heimasíðu",
|
||||
"label.dismiss": "Lat fara",
|
||||
"label.domain": "Økisnavn",
|
||||
"label.edit": "Ger broyting",
|
||||
"label.edit-account": "Broyt brúkara",
|
||||
"label.edit-website": "Broyt heimasíðu",
|
||||
"label.enable-share-url": "Virkja deili leinki",
|
||||
"label.invalid": "Ógilda",
|
||||
"label.invalid-domain": "Ógilt økisnavn",
|
||||
"label.last-days": "Seinastu {x} dagarnar",
|
||||
"label.last-hours": "Seinastu {x} tímanar",
|
||||
"label.last-hours": "Seinastu {x} tímarnar",
|
||||
"label.logged-in-as": "Ritaður inn sum {username}",
|
||||
"label.login": "Rita inn",
|
||||
"label.logout": "Rita út",
|
||||
"label.more": "Meira",
|
||||
"label.name": "Navn",
|
||||
"label.new-password": "Nýtt loyniorð",
|
||||
"label.password": "Loyniorð",
|
||||
"label.passwords-dont-match": "Loyniorðini eru ikki eins",
|
||||
"label.profile": "Brúkari",
|
||||
"label.required": "Krav",
|
||||
"label.profile": "Vangi",
|
||||
"label.realtime": "Beinleiðis",
|
||||
"label.realtime-logs": "Beinleiðis skrá",
|
||||
"label.refresh": "Endurskapa",
|
||||
"label.required": "Kravt",
|
||||
"label.reset": "Nulstilla",
|
||||
"label.save": "Goym",
|
||||
"label.settings": "Stillingar",
|
||||
"label.share-url": "Deil leinku",
|
||||
"label.single-day": "Einkultur dagur",
|
||||
"label.this-month": "Hendan mánan",
|
||||
"label.this-week": "Hesa vikuna",
|
||||
"label.this-year": "Hetta árið",
|
||||
"label.timezone": "Tíðarsona",
|
||||
"label.today": "Í dag",
|
||||
"label.tracking-code": "Spori kota",
|
||||
"label.unknown": "Ókent",
|
||||
"label.username": "Brúkaranavn",
|
||||
"label.view-details": "Vís frágreiðing",
|
||||
"label.websites": "Heimasíður",
|
||||
"message.active-users": "{x} í løtuni {x, plural, one {vitjandi} other { vitjandi }}",
|
||||
"message.confirm-delete": "Ert tú sikkur at tú ynskir at sletta {target}?",
|
||||
"message.copied": "Kopiera!",
|
||||
"message.delete-warning": "Øll data ið er knýtt at verður eisini sletta.",
|
||||
"message.confirm-delete": "Ert tú sikkur at tú ynskir at strika {target}?",
|
||||
"message.copied": "Avrita!",
|
||||
"message.delete-warning": "Øll data ið er knýtt at verður eisini strika.",
|
||||
"message.failure": "Okkurt bleiv gali.",
|
||||
"message.get-share-url": "Fá leinku sum tú kanst deila",
|
||||
"message.get-tracking-code": "Fá sporings kotu",
|
||||
"message.go-to-settings": "Far til stillingar",
|
||||
"message.incorrect-username-password": "Skeivt brúkaranavn/loyniorð.",
|
||||
"message.new-version-available": "A new version of umami {version} is available!",
|
||||
"message.log.visitor": "Vitjandi frá {country} brúkar {browser} á {os} {device}",
|
||||
"message.new-version-available": "Ein nýggj útgava av umami {version} er tøkt!",
|
||||
"message.no-data-available": "Einki data tøk.",
|
||||
"message.no-websites-configured": "Tú hevur ongar heimasíður stillaða til.",
|
||||
"message.page-not-found": "Síðan bleiv ikki funnin.",
|
||||
"message.powered-by": "Powered by {name}",
|
||||
"message.save-success": "Goymt.",
|
||||
"message.share-url": "Hetta er tann almenna leinkan av {target}.",
|
||||
"message.share-url": "Hettar er tann almenna leinkan av {target}.",
|
||||
"message.track-stats": "Fyri at spora hagtøl fyri {target}, koyr kotuna í {head} partin á tínari heimasíðu.",
|
||||
"message.type-delete": "Skriva {delete} í feltið fyri at vátta",
|
||||
"metrics.actions": "Gerðir",
|
||||
|
@ -83,15 +94,6 @@
|
|||
"metrics.pages": "Síðir",
|
||||
"metrics.referrers": "Framsendingar",
|
||||
"metrics.unique-visitors": "Einsýna vitjanir",
|
||||
"metrics.views": "Vitjanir",
|
||||
"metrics.visitors": "Vitjandi",
|
||||
"title.add-account": "Ger brúkara",
|
||||
"title.add-website": "Legg heimasíðu avtrat",
|
||||
"title.change-password": "Skift loyniorð",
|
||||
"title.delete-account": "Sletta brúkara",
|
||||
"title.delete-website": "Sletta heimasíðu",
|
||||
"title.edit-account": "Broyt brúkara",
|
||||
"title.edit-website": "Broyt heimasíðu",
|
||||
"title.share-url": "Deil leinku",
|
||||
"title.tracking-code": "Spori kota"
|
||||
"metrics.views": "Sýningar",
|
||||
"metrics.visitors": "Vitjandi"
|
||||
}
|
||||
|
|
|
@ -1,53 +1,63 @@
|
|||
{
|
||||
"button.add-account": "Ajouter un compte",
|
||||
"button.add-website": "Ajouter un site",
|
||||
"button.back": "Retour",
|
||||
"button.cancel": "Annuler",
|
||||
"button.change-password": "Changer de mot de passse",
|
||||
"button.copy-to-clipboard": "Copier dans le presse papier",
|
||||
"button.date-range": "Date range",
|
||||
"button.delete": "Supprimer",
|
||||
"button.dismiss": "Dismiss",
|
||||
"button.edit": "Modifier",
|
||||
"button.login": "Connexion",
|
||||
"button.more": "Plus",
|
||||
"button.refresh": "Refresh",
|
||||
"button.reset": "Reset",
|
||||
"button.save": "Sauvegarder",
|
||||
"button.single-day": "Single day",
|
||||
"button.view-details": "Voir les details",
|
||||
"label.accounts": "Comptes",
|
||||
"label.add-account": "Ajouter un compte",
|
||||
"label.add-website": "Ajouter un site",
|
||||
"label.administrator": "Administrateur",
|
||||
"label.all": "Tout",
|
||||
"label.all-websites": "Tous les sites web",
|
||||
"label.back": "Retour",
|
||||
"label.cancel": "Annuler",
|
||||
"label.change-password": "Changer le mot de passe",
|
||||
"label.confirm-password": "Confirmation du mot de passe",
|
||||
"label.copy-to-clipboard": "Copier dans le presse papier",
|
||||
"label.current-password": "Mot de passe actuel",
|
||||
"label.custom-range": "Plage personnalisée",
|
||||
"label.custom-range": "Intervalle personnalisé",
|
||||
"label.dashboard": "Tableau de bord",
|
||||
"label.default-date-range": "Default date range",
|
||||
"label.date-range": "Intervalle",
|
||||
"label.default-date-range": "Intervalle par défaut",
|
||||
"label.delete": "Supprimer",
|
||||
"label.delete-account": "Supprimer le compte",
|
||||
"label.delete-website": "Supprimer le site",
|
||||
"label.dismiss": "Ignorer",
|
||||
"label.domain": "Domaine",
|
||||
"label.edit": "Modifier",
|
||||
"label.edit-account": "Modifier le compte",
|
||||
"label.edit-website": "Modifier le site",
|
||||
"label.enable-share-url": "Activer le partage d'URL",
|
||||
"label.invalid": "Invalide",
|
||||
"label.invalid-domain": "Domaine invalide",
|
||||
"label.last-days": "{x} derniers jours",
|
||||
"label.last-hours": "{x} dernières heures",
|
||||
"label.logged-in-as": "Connecté en tant que {username}",
|
||||
"label.login": "Connexion",
|
||||
"label.logout": "Déconnexion",
|
||||
"label.more": "Plus",
|
||||
"label.name": "Nom",
|
||||
"label.new-password": "Nouveau mot de passe",
|
||||
"label.password": "Mot de passe",
|
||||
"label.passwords-dont-match": "Les mots de passe ne correspondent pas",
|
||||
"label.profile": "Profile",
|
||||
"label.profile": "Profil",
|
||||
"label.realtime": "Temps réel",
|
||||
"label.realtime-logs": "Logs en temps réel",
|
||||
"label.refresh": "Rafraîchir",
|
||||
"label.required": "Requis",
|
||||
"label.reset": "Réinitialiser",
|
||||
"label.save": "Sauvegarder",
|
||||
"label.settings": "Paramètres",
|
||||
"label.share-url": "Partager l'URL",
|
||||
"label.single-day": "Journée",
|
||||
"label.this-month": "Ce mois ci",
|
||||
"label.this-week": "Cette semaine",
|
||||
"label.this-year": "Cette année",
|
||||
"label.timezone": "Timezone",
|
||||
"label.timezone": "Fuseau horaire",
|
||||
"label.today": "Aujourd'hui",
|
||||
"label.unknown": "Unknown",
|
||||
"label.tracking-code": "Code de suivi",
|
||||
"label.unknown": "Inconnu",
|
||||
"label.username": "Nom d'utilisateur",
|
||||
"label.view-details": "Voir les details",
|
||||
"label.websites": "Sites",
|
||||
"message.active-users": "{x} {x, plural, one {visiteur} other {visiteurs}} actuellement",
|
||||
"message.confirm-delete": "Êtes-vous sur de vouloir supprimer {target}?",
|
||||
"message.confirm-delete": "Êtes-vous sûr de vouloir supprimer {target} ?",
|
||||
"message.copied": "Copié !",
|
||||
"message.delete-warning": "Toutes les données associées seront également supprimées.",
|
||||
"message.failure": "Un problème est survenu.",
|
||||
|
@ -55,7 +65,8 @@
|
|||
"message.get-tracking-code": "Obtenez le code de suivi",
|
||||
"message.go-to-settings": "Aller aux paramètres",
|
||||
"message.incorrect-username-password": "nom d'utilisateurs/mot de passe incorrect.",
|
||||
"message.new-version-available": "A new version of umami {version} is available!",
|
||||
"message.log.visitor": "Visiteur de {country} utilisant {browser} sur {os} {device}",
|
||||
"message.new-version-available": "Une nouvelle version de umami {version} est disponible !",
|
||||
"message.no-data-available": "Pas de données disponibles.",
|
||||
"message.no-websites-configured": "Vous n'avez configuré aucun site Web.",
|
||||
"message.page-not-found": "Page non trouvée.",
|
||||
|
@ -69,10 +80,10 @@
|
|||
"metrics.bounce-rate": "Taux de rebond",
|
||||
"metrics.browsers": "Navigateurs",
|
||||
"metrics.countries": "Pays",
|
||||
"metrics.device.desktop": "Desktop",
|
||||
"metrics.device.laptop": "Laptop",
|
||||
"metrics.device.mobile": "Mobile",
|
||||
"metrics.device.tablet": "Tablet",
|
||||
"metrics.device.desktop": "Ordinateur",
|
||||
"metrics.device.laptop": "Portable",
|
||||
"metrics.device.mobile": "Téléphone",
|
||||
"metrics.device.tablet": "Tablette",
|
||||
"metrics.devices": "Appareils",
|
||||
"metrics.events": "Événements",
|
||||
"metrics.filter.combined": "Combiné",
|
||||
|
@ -84,14 +95,5 @@
|
|||
"metrics.referrers": "URL Référentes",
|
||||
"metrics.unique-visitors": "Visiteurs uniques",
|
||||
"metrics.views": "Vues",
|
||||
"metrics.visitors": "Visiteurs",
|
||||
"title.add-account": "Ajouter un compte",
|
||||
"title.add-website": "Ajouter un site",
|
||||
"title.change-password": "Changer le mot de passe",
|
||||
"title.delete-account": "Supprimer le compte",
|
||||
"title.delete-website": "Suprimer le site",
|
||||
"title.edit-account": "Modifier le compte",
|
||||
"title.edit-website": "Modifier le site",
|
||||
"title.share-url": "Partager l'URL",
|
||||
"title.tracking-code": "Code de suivi"
|
||||
"metrics.visitors": "Visiteurs"
|
||||
}
|
||||
|
|
|
@ -1,50 +1,60 @@
|
|||
{
|
||||
"button.add-account": "Tambah akun",
|
||||
"button.add-website": "Tambah situs web",
|
||||
"button.back": "Kembali",
|
||||
"button.cancel": "Batal",
|
||||
"button.change-password": "Ganti password",
|
||||
"button.copy-to-clipboard": "Salin ke papan klip",
|
||||
"button.date-range": "Rentang tanggal",
|
||||
"button.delete": "Hapus",
|
||||
"button.dismiss": "Tutup",
|
||||
"button.edit": "Sunting",
|
||||
"button.login": "Masuk",
|
||||
"button.more": "Lebih banyak",
|
||||
"button.refresh": "Segarkan",
|
||||
"button.reset": "Atur ulang",
|
||||
"button.save": "Simpan",
|
||||
"button.single-day": "Sehari",
|
||||
"button.view-details": "Lihat Detil",
|
||||
"label.accounts": "Akun",
|
||||
"label.add-account": "Tambah akun",
|
||||
"label.add-website": "Tambah situs web",
|
||||
"label.administrator": "Pengelola",
|
||||
"label.all": "Semua",
|
||||
"label.all-websites": "Semua website",
|
||||
"label.back": "Kembali",
|
||||
"label.cancel": "Batal",
|
||||
"label.change-password": "Ganti kata sandi",
|
||||
"label.confirm-password": "Konfirmasi kata sandi",
|
||||
"label.copy-to-clipboard": "Salin ke papan klip",
|
||||
"label.current-password": "Kata sandi sekarang",
|
||||
"label.custom-range": "Rentang khusus",
|
||||
"label.dashboard": "Dasbor",
|
||||
"label.default-date-range": "Rentang tanggal default",
|
||||
"label.date-range": "Rentang tanggal",
|
||||
"label.default-date-range": "Rentang tanggal bawaan",
|
||||
"label.delete": "Hapus",
|
||||
"label.delete-account": "Hapus akun",
|
||||
"label.delete-website": "Hapus situs web",
|
||||
"label.dismiss": "Tutup",
|
||||
"label.domain": "Domain",
|
||||
"label.edit": "Sunting",
|
||||
"label.edit-account": "Sunting akun",
|
||||
"label.edit-website": "Sunting situs web",
|
||||
"label.enable-share-url": "Aktifkan URL berbagi",
|
||||
"label.invalid": "Tidak valid",
|
||||
"label.invalid-domain": "Domain tidak valid",
|
||||
"label.last-days": "{x} hari terakhir",
|
||||
"label.last-hours": "{x} jam terakhir",
|
||||
"label.logged-in-as": "Masuk sebagai {username}",
|
||||
"label.login": "Masuk",
|
||||
"label.logout": "Keluar",
|
||||
"label.more": "Lebih banyak",
|
||||
"label.name": "Nama",
|
||||
"label.new-password": "Kata sandi baru",
|
||||
"label.password": "Kata sandi",
|
||||
"label.passwords-dont-match": "Kata sandi tidak cocok",
|
||||
"label.profile": "Profil",
|
||||
"label.realtime": "Waktu nyata",
|
||||
"label.realtime-logs": "Log waktu nyata",
|
||||
"label.refresh": "Segarkan",
|
||||
"label.required": "Wajib",
|
||||
"label.reset": "Atur ulang",
|
||||
"label.save": "Simpan",
|
||||
"label.settings": "Pengaturan",
|
||||
"label.share-url": "Bagikan URL",
|
||||
"label.single-day": "Sehari",
|
||||
"label.this-month": "Bulan ini",
|
||||
"label.this-week": "Minggu ini",
|
||||
"label.this-year": "Tahun ini",
|
||||
"label.timezone": "Zona waktu",
|
||||
"label.today": "Hari ini",
|
||||
"label.tracking-code": "Kode lacak",
|
||||
"label.unknown": "Tidak diketahui",
|
||||
"label.username": "Nama pengguna",
|
||||
"label.view-details": "Lihat Detil",
|
||||
"label.websites": "Situs web",
|
||||
"message.active-users": "{x} pengunjung saat ini",
|
||||
"message.confirm-delete": "Apakah kamu yakin ingin menghapus {target}?",
|
||||
|
@ -55,6 +65,7 @@
|
|||
"message.get-tracking-code": "Dapatkan kode pelacakan",
|
||||
"message.go-to-settings": "Pergi ke pengaturan",
|
||||
"message.incorrect-username-password": "Nama pengguna/kata sandi salah.",
|
||||
"message.log.visitor": "Pengunjung dari {country} dengan {browser} di {device} {os}",
|
||||
"message.new-version-available": "Versi terbaru umami {version} telah tersedia!",
|
||||
"message.no-data-available": "Tidak ada data.",
|
||||
"message.no-websites-configured": "Anda tidak memiliki situs web yang dikonfigurasi.",
|
||||
|
@ -66,8 +77,8 @@
|
|||
"message.type-delete": "Ketikkan {delete} pada kotak di bawah untuk konfirmasi.",
|
||||
"metrics.actions": "Aksi",
|
||||
"metrics.average-visit-time": "Waktu kunjungan rata-rata",
|
||||
"metrics.bounce-rate": "Tingkat bouncing",
|
||||
"metrics.browsers": "Browser",
|
||||
"metrics.bounce-rate": "Rasio pentalan",
|
||||
"metrics.browsers": "Peramban",
|
||||
"metrics.countries": "Negara",
|
||||
"metrics.device.desktop": "Desktop",
|
||||
"metrics.device.laptop": "Laptop",
|
||||
|
@ -77,21 +88,12 @@
|
|||
"metrics.events": "Perihal",
|
||||
"metrics.filter.combined": "Gabungan",
|
||||
"metrics.filter.domain-only": "Hanya domain",
|
||||
"metrics.filter.raw": "Raw",
|
||||
"metrics.filter.raw": "Mentah",
|
||||
"metrics.operating-systems": "Sistem Operasi",
|
||||
"metrics.page-views": "Tampilan halaman",
|
||||
"metrics.pages": "Halaman",
|
||||
"metrics.referrers": "Perujuk",
|
||||
"metrics.unique-visitors": "Pengunjung unik",
|
||||
"metrics.views": "Tampilan",
|
||||
"metrics.visitors": "Pengunjung",
|
||||
"title.add-account": "Tambah akun",
|
||||
"title.add-website": "Tambah situs web",
|
||||
"title.change-password": "Ganti kata sandi",
|
||||
"title.delete-account": "Hapus akun",
|
||||
"title.delete-website": "Hapus situs web",
|
||||
"title.edit-account": "Sunting akun",
|
||||
"title.edit-website": "Sunting situs web",
|
||||
"title.share-url": "Bagikan URL",
|
||||
"title.tracking-code": "Kode lacak"
|
||||
"metrics.visitors": "Pengunjung"
|
||||
}
|
||||
|
|
|
@ -1,50 +1,60 @@
|
|||
{
|
||||
"button.add-account": "アカウントを追加する",
|
||||
"button.add-website": "Webサイトを追加する",
|
||||
"button.back": "戻る",
|
||||
"button.cancel": "キャンセル",
|
||||
"button.change-password": "パスワード変更",
|
||||
"button.copy-to-clipboard": "クリップボードにコピー",
|
||||
"button.date-range": "日付範囲",
|
||||
"button.delete": "削除",
|
||||
"button.dismiss": "無視する",
|
||||
"button.edit": "編集",
|
||||
"button.login": "ログイン",
|
||||
"button.more": "さらに表示",
|
||||
"button.refresh": "更新",
|
||||
"button.reset": "リセット",
|
||||
"button.save": "保存",
|
||||
"button.single-day": "一日のみ",
|
||||
"button.view-details": "詳細を見る",
|
||||
"label.accounts": "アカウント",
|
||||
"label.add-account": "アカウントの追加",
|
||||
"label.add-website": "Webサイトの追加",
|
||||
"label.administrator": "管理者",
|
||||
"label.all": "すべて表示",
|
||||
"label.all-websites": "すべてのWebサイト",
|
||||
"label.back": "戻る",
|
||||
"label.cancel": "キャンセル",
|
||||
"label.change-password": "パスワード変更",
|
||||
"label.confirm-password": "パスワード(確認)",
|
||||
"label.copy-to-clipboard": "クリップボードにコピー",
|
||||
"label.current-password": "現在のパスワード",
|
||||
"label.custom-range": "期間を指定する",
|
||||
"label.dashboard": "ダッシュボード",
|
||||
"label.date-range": "範囲指定",
|
||||
"label.default-date-range": "最初に表示する期間",
|
||||
"label.delete": "削除",
|
||||
"label.delete-account": "アカウントの削除",
|
||||
"label.delete-website": "Webサイトの削除",
|
||||
"label.dismiss": "無視する",
|
||||
"label.domain": "ドメイン",
|
||||
"label.edit": "編集",
|
||||
"label.edit-account": "アカウントの編集",
|
||||
"label.edit-website": "Webサイトの編集",
|
||||
"label.enable-share-url": "共有リンクを有効にする",
|
||||
"label.invalid": "無効",
|
||||
"label.invalid-domain": "無効なドメイン",
|
||||
"label.last-days": "過去{x}日間",
|
||||
"label.last-hours": "過去{x}時間",
|
||||
"label.logged-in-as": "{username}でログイン中",
|
||||
"label.login": "ログイン",
|
||||
"label.logout": "ログアウト",
|
||||
"label.more": "さらに表示",
|
||||
"label.name": "名前",
|
||||
"label.new-password": "新しいパスワード",
|
||||
"label.password": "パスワード",
|
||||
"label.passwords-dont-match": "パスワードが一致しません",
|
||||
"label.profile": "プロファイル",
|
||||
"label.realtime": "リアルタイム",
|
||||
"label.realtime-logs": "リアルタイムログ",
|
||||
"label.refresh": "更新",
|
||||
"label.required": "必須",
|
||||
"label.reset": "リセット",
|
||||
"label.save": "保存",
|
||||
"label.settings": "設定",
|
||||
"label.share-url": "共有リンク",
|
||||
"label.single-day": "一日のみ",
|
||||
"label.this-month": "今月",
|
||||
"label.this-week": "今週",
|
||||
"label.this-year": "今年",
|
||||
"label.timezone": "タイムゾーン",
|
||||
"label.today": "今日",
|
||||
"label.tracking-code": "トラッキングコード",
|
||||
"label.unknown": "不明",
|
||||
"label.username": "ユーザー名",
|
||||
"label.view-details": "詳細を見る",
|
||||
"label.websites": "Webサイト",
|
||||
"message.active-users": "{x}人が閲覧中です。",
|
||||
"message.confirm-delete": "{target}を削除してもよろしいですか?",
|
||||
|
@ -55,11 +65,12 @@
|
|||
"message.get-tracking-code": "トラッキングコードを取得",
|
||||
"message.go-to-settings": "設定する",
|
||||
"message.incorrect-username-password": "ユーザー名/パスワードが正しくありません。",
|
||||
"message.log.visitor": "Visitor from {country} using {browser} on {os} {device}",
|
||||
"message.new-version-available": "新しいバージョン({version})が利用可能です!",
|
||||
"message.no-data-available": "データがありません。",
|
||||
"message.no-websites-configured": "Webサイトが設定されていません。",
|
||||
"message.page-not-found": "ページが見つかりません。",
|
||||
"message.powered-by": "Powered by {name}",
|
||||
"message.powered-by": "このシステムは {name} で実行されています。",
|
||||
"message.save-success": "正常に保存されました。",
|
||||
"message.share-url": "これは {target} の共有リンクです。",
|
||||
"message.track-stats": "{target}のアクセス解析を開始するには、次のコードをWebサイトの{head}セクションへ追加してください。",
|
||||
|
@ -84,14 +95,5 @@
|
|||
"metrics.referrers": "リファラー",
|
||||
"metrics.unique-visitors": "ユニーク訪問者数",
|
||||
"metrics.views": "閲覧数",
|
||||
"metrics.visitors": "訪問者数",
|
||||
"title.add-account": "アカウントの追加",
|
||||
"title.add-website": "Webサイトの追加",
|
||||
"title.change-password": "パスワード変更",
|
||||
"title.delete-account": "アカウントの削除",
|
||||
"title.delete-website": "Webサイトの削除",
|
||||
"title.edit-account": "アカウントの編集",
|
||||
"title.edit-website": "Webサイトの編集",
|
||||
"title.share-url": "共有リンク",
|
||||
"title.tracking-code": "トラッキングコード"
|
||||
"metrics.visitors": "訪問者数"
|
||||
}
|
||||
|
|
|
@ -1,50 +1,60 @@
|
|||
{
|
||||
"button.add-account": "Хэрэглэгч нэмэх",
|
||||
"button.add-website": "Веб нэмэх",
|
||||
"button.back": "Буцах",
|
||||
"button.cancel": "Цуцлах",
|
||||
"button.change-password": "Нууц үг солих",
|
||||
"button.copy-to-clipboard": "Хуулах",
|
||||
"button.date-range": "Хугацааны мужид",
|
||||
"button.delete": "Устгах",
|
||||
"button.dismiss": "Үл хэргэсэх",
|
||||
"button.edit": "Засах",
|
||||
"button.login": "Нэвтрэх",
|
||||
"button.more": "Цааш",
|
||||
"button.refresh": "Сэргээх",
|
||||
"button.reset": "Хуучин хэвд нь оруулах",
|
||||
"button.save": "Хадгалах",
|
||||
"button.single-day": "Нэг өдөр",
|
||||
"button.view-details": "Дэлгэрүүлж харах",
|
||||
"label.accounts": "Хэрэглэгчид",
|
||||
"label.add-account": "Хэрэглэгч нэмэх",
|
||||
"label.add-website": "Веб нэмэх",
|
||||
"label.administrator": "Админ",
|
||||
"label.all": "Бүх",
|
||||
"label.all-websites": "Бүх вебүүд",
|
||||
"label.back": "Буцах",
|
||||
"label.cancel": "Цуцлах",
|
||||
"label.change-password": "Нууц үг солих",
|
||||
"label.confirm-password": "Шинэ нууц үгээ давтах",
|
||||
"label.copy-to-clipboard": "Хуулах",
|
||||
"label.current-password": "Ашиглаж буй нууц үг",
|
||||
"label.custom-range": "Дурын хугацаа",
|
||||
"label.dashboard": "Хянах самбар",
|
||||
"label.date-range": "Хугацааны мужид",
|
||||
"label.default-date-range": "Өгөгдмөл хугацааны муж",
|
||||
"label.delete": "Устгах",
|
||||
"label.delete-account": "Хэрэглэгч устгах",
|
||||
"label.delete-website": "Веб устгах",
|
||||
"label.dismiss": "Үл хэргэсэх",
|
||||
"label.domain": "Домэйн",
|
||||
"label.edit": "Засах",
|
||||
"label.edit-account": "Хэрэглэгч засах",
|
||||
"label.edit-website": "Веб засах",
|
||||
"label.enable-share-url": "Хуваалцах холбоос идэвхжүүлэх",
|
||||
"label.invalid": "Буруу",
|
||||
"label.invalid-domain": "Буруу домэйн",
|
||||
"label.last-days": "Сүүлийн {x} хоног",
|
||||
"label.last-hours": "Сүүлийн {x} цаг",
|
||||
"label.logged-in-as": "{username}-р нэвтэрсэн",
|
||||
"label.login": "Нэвтрэх",
|
||||
"label.logout": "Гарах",
|
||||
"label.more": "Цааш",
|
||||
"label.name": "Нэр",
|
||||
"label.new-password": "Шинэ нууц үг",
|
||||
"label.password": "Нууц үг",
|
||||
"label.passwords-dont-match": "Нууц үг тохирохгүй байна",
|
||||
"label.profile": "Бүртгэл",
|
||||
"label.realtime": "Яг одоо",
|
||||
"label.realtime-logs": "Бодит хугацааны бүртгэл",
|
||||
"label.refresh": "Сэргээх",
|
||||
"label.required": "Шаардлагатай",
|
||||
"label.reset": "Хуучин хэвд нь оруулах",
|
||||
"label.save": "Хадгалах",
|
||||
"label.settings": "Тохиргоо",
|
||||
"label.share-url": "Хуваалцах холбоос",
|
||||
"label.single-day": "Нэг өдөр",
|
||||
"label.this-month": "Энэ сар",
|
||||
"label.this-week": "Энэ долоо хоног",
|
||||
"label.this-year": "Энэ жил",
|
||||
"label.timezone": "Цагийн бүс",
|
||||
"label.today": "Өнөөдөр",
|
||||
"label.tracking-code": "Мөрдөх код",
|
||||
"label.unknown": "Тодорхойгүй",
|
||||
"label.username": "Хэрэглэгчийн нэр",
|
||||
"label.view-details": "Дэлгэрүүлж харах",
|
||||
"label.websites": "Вебүүд",
|
||||
"message.active-users": "одоо {x} {x, plural, one {зочин} other {зочин}} байна",
|
||||
"message.confirm-delete": "Та {target}-г устгахдаа итгэлтэй байна уу?",
|
||||
|
@ -55,11 +65,12 @@
|
|||
"message.get-tracking-code": "Мөрдөх код авах",
|
||||
"message.go-to-settings": "Тохиргоо руу очих",
|
||||
"message.incorrect-username-password": "Буруу хэрэглэгчийн нэр/нууц үг.",
|
||||
"message.log.visitor": "{country} улсаас {os} {device} дээр {browser} хөтөч ашиглан орсон",
|
||||
"message.new-version-available": "Umami-гийн шинэ хувилбар {version} гарсан байна!",
|
||||
"message.no-data-available": "Өгөгдөл алга.",
|
||||
"message.no-websites-configured": "Та ямар нэгэн веб тохируулаагүй байна.",
|
||||
"message.page-not-found": "Хуудас олдсонгүй.",
|
||||
"message.powered-by": "Powered by {name}",
|
||||
"message.powered-by": "{name} дээр суурилсан",
|
||||
"message.save-success": "Амжилттай хадгаллаа.",
|
||||
"message.share-url": "{target}-г нийтэд хуваалцах холбоос.",
|
||||
"message.track-stats": "{target} вебийн статистикийг бүртгэхийн тулд доорх кодыг вебийнхээ {head} хэсэгт байрлуулна уу.",
|
||||
|
@ -84,14 +95,5 @@
|
|||
"metrics.referrers": "Чиглүүлэгч",
|
||||
"metrics.unique-visitors": "Зочид",
|
||||
"metrics.views": "Үзсэн",
|
||||
"metrics.visitors": "Зочид",
|
||||
"title.add-account": "Хэрэглэгч нэмэх",
|
||||
"title.add-website": "Веб нэмэх",
|
||||
"title.change-password": "Нууц үг солих",
|
||||
"title.delete-account": "Хэрэглэгч устгах",
|
||||
"title.delete-website": "Веб устгах",
|
||||
"title.edit-account": "Хэрэглэгч засах",
|
||||
"title.edit-website": "Веб засах",
|
||||
"title.share-url": "Хуваалцах холбоос",
|
||||
"title.tracking-code": "Мөрдөх код"
|
||||
"metrics.visitors": "Зочид"
|
||||
}
|
||||
|
|
|
@ -1,50 +1,60 @@
|
|||
{
|
||||
"button.add-account": "Legg til konto",
|
||||
"button.add-website": "Legg til nettsted",
|
||||
"button.back": "Tilbake",
|
||||
"button.cancel": "Avvis",
|
||||
"button.change-password": "Bytt passord",
|
||||
"button.copy-to-clipboard": "Kopier til utklippstavle",
|
||||
"button.date-range": "Datointervall",
|
||||
"button.delete": "Slett",
|
||||
"button.dismiss": "Avbryt",
|
||||
"button.edit": "Rediger",
|
||||
"button.login": "Logg inn",
|
||||
"button.more": "Mer",
|
||||
"button.refresh": "Oppdater",
|
||||
"button.reset": "Nullstill",
|
||||
"button.save": "Lagre",
|
||||
"button.single-day": "Enkelt dag",
|
||||
"button.view-details": "Vis detaljer",
|
||||
"label.accounts": "Kontoer",
|
||||
"label.add-account": "Legg til konto",
|
||||
"label.add-website": "Legg til nettsted",
|
||||
"label.administrator": "Administrator",
|
||||
"label.all": "Alle",
|
||||
"label.all-websites": "Alle nettsteder",
|
||||
"label.back": "Tilbake",
|
||||
"label.cancel": "Avvis",
|
||||
"label.change-password": "Bytt passord",
|
||||
"label.confirm-password": "Godkjenn passord",
|
||||
"label.copy-to-clipboard": "Kopier til utklippstavle",
|
||||
"label.current-password": "Nåværende passord",
|
||||
"label.custom-range": "Egendefinert utvalg",
|
||||
"label.dashboard": "Dashboard",
|
||||
"label.date-range": "Datointervall",
|
||||
"label.default-date-range": "Standard datoperiode",
|
||||
"label.delete": "Slett",
|
||||
"label.delete-account": "Slett konto",
|
||||
"label.delete-website": "Slett nettstedet",
|
||||
"label.dismiss": "Avbryt",
|
||||
"label.domain": "Domene",
|
||||
"label.edit": "Rediger",
|
||||
"label.edit-account": "Rediger konto",
|
||||
"label.edit-website": "Rediger nettsted",
|
||||
"label.enable-share-url": "Aktiver delings-URL",
|
||||
"label.invalid": "Ugyldig",
|
||||
"label.invalid-domain": "Ugyldig domene",
|
||||
"label.last-days": "Siste {x} dager",
|
||||
"label.last-hours": "Siste {x} timer",
|
||||
"label.logged-in-as": "Logget på som {brukernavn}",
|
||||
"label.login": "Logg inn",
|
||||
"label.logout": "Logg ut",
|
||||
"label.more": "Mer",
|
||||
"label.name": "Navn",
|
||||
"label.new-password": "Nytt passord",
|
||||
"label.password": "Passord",
|
||||
"label.passwords-dont-match": "Passordene er ikke like",
|
||||
"label.profile": "Profil",
|
||||
"label.realtime": "Sanntid",
|
||||
"label.realtime-logs": "Sanntidslogger",
|
||||
"label.refresh": "Oppdater",
|
||||
"label.required": "Påkrevd",
|
||||
"label.reset": "Nullstill",
|
||||
"label.save": "Lagre",
|
||||
"label.settings": "Innstillinger",
|
||||
"label.share-url": "Del URL",
|
||||
"label.single-day": "Enkelt dag",
|
||||
"label.this-month": "Denne måneden",
|
||||
"label.this-week": "Denne uka",
|
||||
"label.this-year": "I år",
|
||||
"label.timezone": "Tidssone",
|
||||
"label.today": "I dag",
|
||||
"label.tracking-code": "Sporingskode",
|
||||
"label.unknown": "Ukjent",
|
||||
"label.username": "Brukernavn",
|
||||
"label.view-details": "Vis detaljer",
|
||||
"label.websites": "Nettsteder",
|
||||
"message.active-users": "{x} {x, plural, one {besøkende} other {besøkende}} nå",
|
||||
"message.confirm-delete": "Er du sikker på at du vil slette {target}?",
|
||||
|
@ -55,6 +65,7 @@
|
|||
"message.get-tracking-code": "Få sporingskode",
|
||||
"message.go-to-settings": "Gå til innstillinger",
|
||||
"message.incorrect-username-password": "Ugyldig brukernavn/passord.",
|
||||
"message.log.visitor": "Besøkende fra {country} med {browser} på {os} {device}",
|
||||
"message.new-version-available": "En ny versjon av umami {version} er tilgjengelig!",
|
||||
"message.no-data-available": "Ingen data tilgjengelig.",
|
||||
"message.no-websites-configured": "Du har ikke satt opp noen nettsteder.",
|
||||
|
@ -69,8 +80,8 @@
|
|||
"metrics.bounce-rate": "Avvisningsfrekvens",
|
||||
"metrics.browsers": "Nettlesere",
|
||||
"metrics.countries": "Land",
|
||||
"metrics.device.desktop": "Desktop",
|
||||
"metrics.device.laptop": "Laptop",
|
||||
"metrics.device.desktop": "Stasjonær",
|
||||
"metrics.device.laptop": "Bærbar",
|
||||
"metrics.device.mobile": "Mobiltelefon",
|
||||
"metrics.device.tablet": "Nettbrett",
|
||||
"metrics.devices": "Enheter",
|
||||
|
@ -84,14 +95,5 @@
|
|||
"metrics.referrers": "Referanser",
|
||||
"metrics.unique-visitors": "Unike besøkende",
|
||||
"metrics.views": "Visninger",
|
||||
"metrics.visitors": "Besøkende",
|
||||
"title.add-account": "Legg til konto",
|
||||
"title.add-website": "Legg til nettsted",
|
||||
"title.change-password": "Bytt passord",
|
||||
"title.delete-account": "Slett konto",
|
||||
"title.delete-website": "Slett nettstedet",
|
||||
"title.edit-account": "Rediger konto",
|
||||
"title.edit-website": "Rediger nettsted",
|
||||
"title.share-url": "Del URL",
|
||||
"title.tracking-code": "Sporingskode"
|
||||
"metrics.visitors": "Besøkende"
|
||||
}
|
||||
|
|
|
@ -1,50 +1,60 @@
|
|||
{
|
||||
"button.add-account": "Account toevoegen",
|
||||
"button.add-website": "Website toevoegen",
|
||||
"button.back": "Terug",
|
||||
"button.cancel": "Annuleren",
|
||||
"button.change-password": "Wachtwoord wijzigen",
|
||||
"button.copy-to-clipboard": "Kopiëer naar klembord",
|
||||
"button.date-range": "Datumbereik",
|
||||
"button.delete": "Verwijderen",
|
||||
"button.dismiss": "Negeren",
|
||||
"button.edit": "Bewerken",
|
||||
"button.login": "Inloggen",
|
||||
"button.more": "Toon meer",
|
||||
"button.refresh": "Vernieuwen",
|
||||
"button.reset": "Resetten",
|
||||
"button.save": "Opslaan",
|
||||
"button.single-day": "Enkele dag",
|
||||
"button.view-details": "Meer details",
|
||||
"label.accounts": "Gebruikers",
|
||||
"label.add-account": "Account toevoegen",
|
||||
"label.add-website": "Website toevoegen",
|
||||
"label.administrator": "Administrator",
|
||||
"label.all": "Alles",
|
||||
"label.all-websites": "Alle websites",
|
||||
"label.back": "Terug",
|
||||
"label.cancel": "Annuleren",
|
||||
"label.change-password": "Wachtwoord wijzigen",
|
||||
"label.confirm-password": "Wachtwoord bevestigen",
|
||||
"label.copy-to-clipboard": "Kopiëer naar klembord",
|
||||
"label.current-password": "Huidig wachtwoord",
|
||||
"label.custom-range": "Aangepast bereik",
|
||||
"label.dashboard": "Overzicht",
|
||||
"label.date-range": "Datumbereik",
|
||||
"label.default-date-range": "Standaard bereik",
|
||||
"label.delete": "Verwijderen",
|
||||
"label.delete-account": "Account verwijderen",
|
||||
"label.delete-website": "Website verwijderen",
|
||||
"label.dismiss": "Negeren",
|
||||
"label.domain": "Domein",
|
||||
"label.edit": "Bewerken",
|
||||
"label.edit-account": "Account bewerken",
|
||||
"label.edit-website": "Website bewerken",
|
||||
"label.enable-share-url": "Sta delen via openbare URL toe",
|
||||
"label.invalid": "Ongeldig",
|
||||
"label.invalid-domain": "Ongeldig domein",
|
||||
"label.last-days": "Laatste {x} dagen",
|
||||
"label.last-hours": "Laatste {x} uur",
|
||||
"label.logged-in-as": "Ingelogd als {username}",
|
||||
"label.login": "Inloggen",
|
||||
"label.logout": "Uitloggen",
|
||||
"label.more": "Toon meer",
|
||||
"label.name": "Naam",
|
||||
"label.new-password": "Nieuw wachtwoord",
|
||||
"label.password": "Wachtwoord",
|
||||
"label.passwords-dont-match": "Wachtwoorden komen niet overeen",
|
||||
"label.profile": "Profiel",
|
||||
"label.realtime": "Actueel",
|
||||
"label.realtime-logs": "Actueel logboek",
|
||||
"label.refresh": "Vernieuwen",
|
||||
"label.required": "Verplicht",
|
||||
"label.reset": "Resetten",
|
||||
"label.save": "Opslaan",
|
||||
"label.settings": "Instellingen",
|
||||
"label.share-url": "URL delen",
|
||||
"label.single-day": "Enkele dag",
|
||||
"label.this-month": "Deze maand",
|
||||
"label.this-week": "Deze week",
|
||||
"label.this-year": "Dit jaar",
|
||||
"label.timezone": "Tijdzone",
|
||||
"label.today": "Vandaag",
|
||||
"label.tracking-code": "Volgcode",
|
||||
"label.unknown": "Onbekend",
|
||||
"label.username": "Gebruikersnaam",
|
||||
"label.view-details": "Meer details",
|
||||
"label.websites": "Websites",
|
||||
"message.active-users": "{x} actieve {x, plural, one {bezoeker} other {bezoekers}}",
|
||||
"message.confirm-delete": "Weet je zeker dat je {target} wilt verwijderen?",
|
||||
|
@ -55,6 +65,7 @@
|
|||
"message.get-tracking-code": "Tracking code",
|
||||
"message.go-to-settings": "Naar instellingen",
|
||||
"message.incorrect-username-password": "Incorrecte gebruikersnaam/wachtwoord.",
|
||||
"message.log.visitor": "Bezoeker uit {country} met {browser} op een {os} {device}",
|
||||
"message.new-version-available": "Een nieuwe versie van umami {version} is beschikbaar!",
|
||||
"message.no-data-available": "Geen gegevens beschikbaar.",
|
||||
"message.no-websites-configured": "Je hebt geen websites ingesteld.",
|
||||
|
@ -84,14 +95,5 @@
|
|||
"metrics.referrers": "Verwijzers",
|
||||
"metrics.unique-visitors": "Unieke bezoekers",
|
||||
"metrics.views": "Weergaven",
|
||||
"metrics.visitors": "Bezoekers",
|
||||
"title.add-account": "Account toevoegen",
|
||||
"title.add-website": "Website toevoegen",
|
||||
"title.change-password": "Wachtwoord wijzigen",
|
||||
"title.delete-account": "Account verwijderen",
|
||||
"title.delete-website": "Website verwijderen",
|
||||
"title.edit-account": "Account bewerken",
|
||||
"title.edit-website": "Website bewerken",
|
||||
"title.share-url": "URL delen",
|
||||
"title.tracking-code": "Tracking code"
|
||||
"metrics.visitors": "Bezoekers"
|
||||
}
|
||||
|
|
|
@ -1,50 +1,60 @@
|
|||
{
|
||||
"button.add-account": "Adicionar conta",
|
||||
"button.add-website": "Adicionar website",
|
||||
"button.back": "Voltar",
|
||||
"button.cancel": "Cancelar",
|
||||
"button.change-password": "Alterar palavra-passe",
|
||||
"button.copy-to-clipboard": "Copiar para a área de transferência",
|
||||
"button.date-range": "Intervalo de datas",
|
||||
"button.delete": "Eliminar",
|
||||
"button.dismiss": "Ignorar",
|
||||
"button.edit": "Editar",
|
||||
"button.login": "Iniciar sessão",
|
||||
"button.more": "Mais",
|
||||
"button.refresh": "Atualizar",
|
||||
"button.reset": "Repor",
|
||||
"button.save": "Guardar",
|
||||
"button.single-day": "Dia único",
|
||||
"button.view-details": "Ver detalhes",
|
||||
"label.accounts": "Contas",
|
||||
"label.add-account": "Adicionar conta",
|
||||
"label.add-website": "Adicionar website",
|
||||
"label.administrator": "Administrador",
|
||||
"label.all": "Todos",
|
||||
"label.all-websites": "Todos os websites",
|
||||
"label.back": "Voltar",
|
||||
"label.cancel": "Cancelar",
|
||||
"label.change-password": "Alterar palavra-passe",
|
||||
"label.confirm-password": "Confirmar palavra-passe",
|
||||
"label.copy-to-clipboard": "Copiar para a área de transferência",
|
||||
"label.current-password": "Palavra-passe atual",
|
||||
"label.custom-range": "Intervalo personalizado",
|
||||
"label.dashboard": "Dashboard",
|
||||
"label.date-range": "Intervalo de datas",
|
||||
"label.default-date-range": "Intervalo de datas predefinido",
|
||||
"label.delete": "Eliminar",
|
||||
"label.delete-account": "Eliminar conta",
|
||||
"label.delete-website": "Eliminar website",
|
||||
"label.dismiss": "Ignorar",
|
||||
"label.domain": "Domínio",
|
||||
"label.edit": "Editar",
|
||||
"label.edit-account": "Editar conta",
|
||||
"label.edit-website": "Editar website",
|
||||
"label.enable-share-url": "Ativar link de partilha",
|
||||
"label.invalid": "Inválido",
|
||||
"label.invalid-domain": "Domínio inválido",
|
||||
"label.last-days": "Últimos {x} dias",
|
||||
"label.last-hours": "Últimas {x} horas",
|
||||
"label.logged-in-as": "Sessão iniciada como {username}",
|
||||
"label.login": "Iniciar sessão",
|
||||
"label.logout": "Sair",
|
||||
"label.more": "Mais",
|
||||
"label.name": "Nome",
|
||||
"label.new-password": "Nova palavra-passe",
|
||||
"label.password": "Palavra-passe",
|
||||
"label.passwords-dont-match": "Palavra-passes não correspondem",
|
||||
"label.profile": "Perfil",
|
||||
"label.realtime": "Tempo real",
|
||||
"label.realtime-logs": "Relatório em tempo real",
|
||||
"label.refresh": "Atualizar",
|
||||
"label.required": "Obrigatório",
|
||||
"label.reset": "Repor",
|
||||
"label.save": "Guardar",
|
||||
"label.settings": "Definições",
|
||||
"label.share-url": "Partilhar link",
|
||||
"label.single-day": "Dia único",
|
||||
"label.this-month": "Este mês",
|
||||
"label.this-week": "Esta semana",
|
||||
"label.this-year": "Este ano",
|
||||
"label.timezone": "Fuso horário",
|
||||
"label.today": "Hoje",
|
||||
"label.tracking-code": "Código de tracking",
|
||||
"label.unknown": "Desconhecido",
|
||||
"label.username": "Nome de utilizador",
|
||||
"label.view-details": "Ver detalhes",
|
||||
"label.websites": "Websites",
|
||||
"message.active-users": "{x} {x, plural, one {visitante} other {visitantes}} neste momento",
|
||||
"message.confirm-delete": "Tens a certeza que queres eliminar {target}?",
|
||||
|
@ -55,6 +65,7 @@
|
|||
"message.get-tracking-code": "Obter código de tracking",
|
||||
"message.go-to-settings": "Ir para as definições",
|
||||
"message.incorrect-username-password": "Nome de utilizador/palavra-passe incorretos.",
|
||||
"message.log.visitor": "Visitante de {country} a usar {browser} no {device} {os}",
|
||||
"message.new-version-available": "Uma nova versão de umami {version} está disponível!",
|
||||
"message.no-data-available": "Sem dados disponíveis.",
|
||||
"message.no-websites-configured": "Não tens nenhum website configurado.",
|
||||
|
@ -84,14 +95,5 @@
|
|||
"metrics.referrers": "Referrers",
|
||||
"metrics.unique-visitors": "Visitantes únicos",
|
||||
"metrics.views": "Visualizações",
|
||||
"metrics.visitors": "Visitantes",
|
||||
"title.add-account": "Adicionar conta",
|
||||
"title.add-website": "Adicionar website",
|
||||
"title.change-password": "Alterar palavra-passe",
|
||||
"title.delete-account": "Eliminar conta",
|
||||
"title.delete-website": "Eliminar website",
|
||||
"title.edit-account": "Editar conta",
|
||||
"title.edit-website": "Editar website",
|
||||
"title.share-url": "Partilhar link",
|
||||
"title.tracking-code": "Código de tracking"
|
||||
"metrics.visitors": "Visitantes"
|
||||
}
|
||||
|
|
|
@ -1,50 +1,60 @@
|
|||
{
|
||||
"button.add-account": "Adaugă cont",
|
||||
"button.add-website": "Adaugă site web",
|
||||
"button.back": "Înapoi",
|
||||
"button.cancel": "Anulează",
|
||||
"button.change-password": "Schimbă parola",
|
||||
"button.copy-to-clipboard": "Copiază în clipboard",
|
||||
"button.date-range": "Interval de date",
|
||||
"button.delete": "Șterge",
|
||||
"button.dismiss": "Renunță",
|
||||
"button.edit": "Editare",
|
||||
"button.login": "Autentificare",
|
||||
"button.more": "Mai mult",
|
||||
"button.refresh": "Reîmprospătare",
|
||||
"button.reset": "Resetează",
|
||||
"button.save": "Salvează",
|
||||
"button.single-day": "O singură zi",
|
||||
"button.view-details": "Vizualizare detalii",
|
||||
"label.accounts": "Conturi",
|
||||
"label.add-account": "Adăugare cont",
|
||||
"label.add-website": "Adăugare site web",
|
||||
"label.administrator": "Administrator",
|
||||
"label.all": "All",
|
||||
"label.all-websites": "All websites",
|
||||
"label.back": "Înapoi",
|
||||
"label.cancel": "Anulează",
|
||||
"label.change-password": "Schimbare parolă",
|
||||
"label.confirm-password": "Confirmare parolă",
|
||||
"label.copy-to-clipboard": "Copiază în clipboard",
|
||||
"label.current-password": "Parola curentă",
|
||||
"label.custom-range": "Interval personalizat",
|
||||
"label.dashboard": "Tablou de bord",
|
||||
"label.date-range": "Interval de date",
|
||||
"label.default-date-range": "Interval de date implicit",
|
||||
"label.delete": "Șterge",
|
||||
"label.delete-account": "Ștergere cont",
|
||||
"label.delete-website": "Ștergere site web",
|
||||
"label.dismiss": "Renunță",
|
||||
"label.domain": "Domeniu",
|
||||
"label.edit": "Editare",
|
||||
"label.edit-account": "Editare cont",
|
||||
"label.edit-website": "Editare site web",
|
||||
"label.enable-share-url": "Activare adresa URL de distribuire",
|
||||
"label.invalid": "Invalid",
|
||||
"label.invalid-domain": "Invalid domain",
|
||||
"label.last-days": "Ultimele {x} zile",
|
||||
"label.last-hours": "Ultimele {x} ore",
|
||||
"label.logged-in-as": "Autentificat ca {username}",
|
||||
"label.login": "Autentificare",
|
||||
"label.logout": "Dezautentificare",
|
||||
"label.more": "Mai mult",
|
||||
"label.name": "Nume",
|
||||
"label.new-password": "Parola nouă",
|
||||
"label.password": "Parolă",
|
||||
"label.passwords-dont-match": "Parolele nu se potrivesc",
|
||||
"label.profile": "Profil",
|
||||
"label.realtime": "Realtime",
|
||||
"label.realtime-logs": "Realtime logs",
|
||||
"label.refresh": "Reîmprospătare",
|
||||
"label.required": "Obligatoriu",
|
||||
"label.reset": "Resetează",
|
||||
"label.save": "Salvează",
|
||||
"label.settings": "Setări",
|
||||
"label.share-url": "Partajare URL",
|
||||
"label.single-day": "O singură zi",
|
||||
"label.this-month": "Această lună",
|
||||
"label.this-week": "Această săptămână",
|
||||
"label.this-year": "Acest an",
|
||||
"label.timezone": "Fus orar",
|
||||
"label.today": "Astăzi",
|
||||
"label.tracking-code": "Cod de urmărire",
|
||||
"label.unknown": "Necunoscut",
|
||||
"label.username": "Username",
|
||||
"label.view-details": "Vizualizare detalii",
|
||||
"label.websites": "Site-uri web",
|
||||
"message.active-users": "{x} {x, plural, one {vizitator activ} other {vizitatori activi}}",
|
||||
"message.confirm-delete": "Sunteți sigur că doriți să ștergeți {target}?",
|
||||
|
@ -55,6 +65,7 @@
|
|||
"message.get-tracking-code": "Obține codul de urmărire",
|
||||
"message.go-to-settings": "Mergi la Setări",
|
||||
"message.incorrect-username-password": "Username/parolă incorecte.",
|
||||
"message.log.visitor": "Visitor from {country} using {browser} on {os} {device}",
|
||||
"message.new-version-available": "Este disponibilă o nouă versiune {version} de umami!",
|
||||
"message.no-data-available": "Nicio informație disponibilă.",
|
||||
"message.no-websites-configured": "Nu aveți niciun site web configurat.",
|
||||
|
@ -84,14 +95,5 @@
|
|||
"metrics.referrers": "Site-uri de proveniență",
|
||||
"metrics.unique-visitors": "Vizitatori unici",
|
||||
"metrics.views": "Vizualizări",
|
||||
"metrics.visitors": "Vizitatori",
|
||||
"title.add-account": "Adăugare cont",
|
||||
"title.add-website": "Adăugare site web",
|
||||
"title.change-password": "Schimbare parolă",
|
||||
"title.delete-account": "Ștergere cont",
|
||||
"title.delete-website": "Ștergere site web",
|
||||
"title.edit-account": "Editare cont",
|
||||
"title.edit-website": "Editare site web",
|
||||
"title.share-url": "Partajare URL",
|
||||
"title.tracking-code": "Cod de urmărire"
|
||||
"metrics.visitors": "Vizitatori"
|
||||
}
|
||||
|
|
|
@ -1,50 +1,60 @@
|
|||
{
|
||||
"button.add-account": "Добавить аккаунт",
|
||||
"button.add-website": "Добавить сайт",
|
||||
"button.back": "Назад",
|
||||
"button.cancel": "Отменить",
|
||||
"button.change-password": "Изменить пароль",
|
||||
"button.copy-to-clipboard": "Скопировать в буфер обмена",
|
||||
"button.date-range": "Диапазон дат",
|
||||
"button.delete": "Удалить",
|
||||
"button.dismiss": "Отклонить",
|
||||
"button.edit": "Редактировать",
|
||||
"button.login": "Войти",
|
||||
"button.more": "Больше",
|
||||
"button.refresh": "Обновить",
|
||||
"button.reset": "Сбросить",
|
||||
"button.save": "Сохранить",
|
||||
"button.single-day": "Один день",
|
||||
"button.view-details": "Посмотреть детали",
|
||||
"label.accounts": "Аккаунты",
|
||||
"label.add-account": "Добавить аккаунт",
|
||||
"label.add-website": "Добавить сайт",
|
||||
"label.administrator": "Администратор",
|
||||
"label.all": "Все",
|
||||
"label.all-websites": "Все сайты",
|
||||
"label.back": "Назад",
|
||||
"label.cancel": "Отменить",
|
||||
"label.change-password": "Изменить пароль",
|
||||
"label.confirm-password": "Подтвердить пароль",
|
||||
"label.copy-to-clipboard": "Скопировать в буфер обмена",
|
||||
"label.current-password": "Текущий пароль",
|
||||
"label.custom-range": "Другой период",
|
||||
"label.dashboard": "Информационная панель",
|
||||
"label.date-range": "Диапазон дат",
|
||||
"label.default-date-range": "Диапазон дат по-умолчанию",
|
||||
"label.delete": "Удалить",
|
||||
"label.delete-account": "Удалить аккаунт",
|
||||
"label.delete-website": "Удалить сайт",
|
||||
"label.dismiss": "Отклонить",
|
||||
"label.domain": "Домен",
|
||||
"label.edit": "Редактировать",
|
||||
"label.edit-account": "Редактировать аккаунт",
|
||||
"label.edit-website": "Редактировать сайт",
|
||||
"label.enable-share-url": "Разрешить делиться ссылкой",
|
||||
"label.invalid": "Некорректный",
|
||||
"label.invalid-domain": "Некорректный домен",
|
||||
"label.last-days": "Последние {x} дней",
|
||||
"label.last-hours": "Последние {x} часа",
|
||||
"label.logged-in-as": "Вы вошли как {username}",
|
||||
"label.login": "Войти",
|
||||
"label.logout": "Выйти",
|
||||
"label.more": "Больше",
|
||||
"label.name": "Имя",
|
||||
"label.new-password": "Новый пароль",
|
||||
"label.password": "Пароль",
|
||||
"label.passwords-dont-match": "Пароли не совпадают",
|
||||
"label.profile": "Профиль",
|
||||
"label.realtime": "Реальное время",
|
||||
"label.realtime-logs": "Логи в реальном времени",
|
||||
"label.refresh": "Обновить",
|
||||
"label.required": "Обязательное",
|
||||
"label.reset": "Сбросить",
|
||||
"label.save": "Сохранить",
|
||||
"label.settings": "Настройки",
|
||||
"label.share-url": "Поделиться ссылкой",
|
||||
"label.single-day": "Один день",
|
||||
"label.this-month": "Этот месяц",
|
||||
"label.this-week": "Эта неделя",
|
||||
"label.this-year": "Этот год",
|
||||
"label.timezone": "Часовой пояс",
|
||||
"label.today": "Сегодня",
|
||||
"label.tracking-code": "Код отслеживания",
|
||||
"label.unknown": "Неизвестно",
|
||||
"label.username": "Имя пользователя",
|
||||
"label.view-details": "Посмотреть детали",
|
||||
"label.websites": "Сайты",
|
||||
"message.active-users": "{x} текущих посетителей",
|
||||
"message.confirm-delete": "Вы уверены, что хотите удалить {target}?",
|
||||
|
@ -55,6 +65,7 @@
|
|||
"message.get-tracking-code": "Получить код отслеживания",
|
||||
"message.go-to-settings": "Перейти к настройкам",
|
||||
"message.incorrect-username-password": "Неверное имя пользователя/пароль.",
|
||||
"message.log.visitor": "Посетитель из {country} используя {browser} на {os} {device}",
|
||||
"message.new-version-available": "Доступна новая версия umami {version}",
|
||||
"message.no-data-available": "Нет данных.",
|
||||
"message.no-websites-configured": "У вас нет настроенных сайтов.",
|
||||
|
@ -84,14 +95,5 @@
|
|||
"metrics.referrers": "Источники",
|
||||
"metrics.unique-visitors": "Уникальные посетители",
|
||||
"metrics.views": "Просмотры",
|
||||
"metrics.visitors": "Посетители",
|
||||
"title.add-account": "Добавить аккаунт",
|
||||
"title.add-website": "Добавить сайт",
|
||||
"title.change-password": "Изменить пароль",
|
||||
"title.delete-account": "Удалить аккаунт",
|
||||
"title.delete-website": "Удалить сайт",
|
||||
"title.edit-account": "Редактировать аккаунт",
|
||||
"title.edit-website": "Редактировать сайт",
|
||||
"title.share-url": "Поделиться ссылкой",
|
||||
"title.tracking-code": "Код отслеживания"
|
||||
"metrics.visitors": "Посетители"
|
||||
}
|
||||
|
|
|
@ -1,50 +1,60 @@
|
|||
{
|
||||
"button.add-account": "Lägg till konto",
|
||||
"button.add-website": "Lägg till webbsajt",
|
||||
"button.back": "Tillbaka",
|
||||
"button.cancel": "Avbryt",
|
||||
"button.change-password": "Byt lösenord",
|
||||
"button.copy-to-clipboard": "Kopiera till urklipp",
|
||||
"button.date-range": "Datumomfång",
|
||||
"button.delete": "Radera",
|
||||
"button.dismiss": "Dismiss",
|
||||
"button.edit": "Redigera",
|
||||
"button.login": "Logga in",
|
||||
"button.more": "Mer",
|
||||
"button.refresh": "Uppdatera",
|
||||
"button.reset": "Återställ",
|
||||
"button.save": "Spara",
|
||||
"button.single-day": "En dag",
|
||||
"button.view-details": "Visa detaljer",
|
||||
"label.accounts": "Konton",
|
||||
"label.add-account": "Lägg till konto",
|
||||
"label.add-website": "Lägg till webbsajt",
|
||||
"label.administrator": "Administratör",
|
||||
"label.all": "Alla",
|
||||
"label.all-websites": "Alla sajter",
|
||||
"label.back": "Tillbaka",
|
||||
"label.cancel": "Avbryt",
|
||||
"label.change-password": "Byt lösenord",
|
||||
"label.confirm-password": "Bekräfta lösenord",
|
||||
"label.copy-to-clipboard": "Kopiera till urklipp",
|
||||
"label.current-password": "Nuvarande lösenord",
|
||||
"label.custom-range": "Anpassat urval",
|
||||
"label.dashboard": "Instrumentpanel",
|
||||
"label.dashboard": "Översikt",
|
||||
"label.date-range": "Datumomfång",
|
||||
"label.default-date-range": "Standard datum-urval",
|
||||
"label.delete": "Radera",
|
||||
"label.delete-account": "Radera konto",
|
||||
"label.delete-website": "Radera webbsajt",
|
||||
"label.dismiss": "Avbryt",
|
||||
"label.domain": "Domän",
|
||||
"label.edit": "Redigera",
|
||||
"label.edit-account": "Redigera konto",
|
||||
"label.edit-website": "Redigera webbsajt",
|
||||
"label.enable-share-url": "Aktivera delnings-URL",
|
||||
"label.invalid": "Ogiltig",
|
||||
"label.invalid-domain": "Ogiltig domän",
|
||||
"label.last-days": "Senaste {x} dagarna",
|
||||
"label.last-hours": "Senaste {x} timmarna",
|
||||
"label.logged-in-as": "Inloggad som {username}",
|
||||
"label.login": "Logga in",
|
||||
"label.logout": "Logga ut",
|
||||
"label.more": "Mer",
|
||||
"label.name": "Namn",
|
||||
"label.new-password": "Nytt lösenord",
|
||||
"label.password": "Lösenord",
|
||||
"label.passwords-dont-match": "Lösenorden är inte samma",
|
||||
"label.profile": "Profil",
|
||||
"label.realtime": "Realtid",
|
||||
"label.realtime-logs": "Realtidsloggar",
|
||||
"label.refresh": "Uppdatera",
|
||||
"label.required": "Krävs",
|
||||
"label.reset": "Återställ",
|
||||
"label.save": "Spara",
|
||||
"label.settings": "Inställningar",
|
||||
"label.share-url": "Delnings-URL",
|
||||
"label.single-day": "En dag",
|
||||
"label.this-month": "Denna månad",
|
||||
"label.this-week": "Denna vecka",
|
||||
"label.this-year": "Detta år",
|
||||
"label.timezone": "Tidszon",
|
||||
"label.today": "Idag",
|
||||
"label.tracking-code": "Spårningskod",
|
||||
"label.unknown": "Okänd",
|
||||
"label.username": "Användarnamn",
|
||||
"label.view-details": "Visa detaljer",
|
||||
"label.websites": "Webbsajt",
|
||||
"message.active-users": "{x} {x, plural, one {besökare} other {besökare}} just nu",
|
||||
"message.confirm-delete": "Är du säker på att du vill radera {target}?",
|
||||
|
@ -55,7 +65,8 @@
|
|||
"message.get-tracking-code": "Visa spårningskod",
|
||||
"message.go-to-settings": "Gå till inställningar",
|
||||
"message.incorrect-username-password": "Felaktikt användarnamn/lösenord.",
|
||||
"message.new-version-available": "A new version of umami {version} is available!",
|
||||
"message.log.visitor": "Besökare från {country} med {browser} på {os} {device}",
|
||||
"message.new-version-available": "En ny version av umami {version} är tillgänglig!",
|
||||
"message.no-data-available": "Ingen data tillgänglig.",
|
||||
"message.no-websites-configured": "Du har inga webbsajter.",
|
||||
"message.page-not-found": "Sidan kan inte hittas.",
|
||||
|
@ -69,8 +80,8 @@
|
|||
"metrics.bounce-rate": "Avvisningfrekvens",
|
||||
"metrics.browsers": "Webbläsare",
|
||||
"metrics.countries": "Länder",
|
||||
"metrics.device.desktop": "Desktop",
|
||||
"metrics.device.laptop": "Laptop",
|
||||
"metrics.device.desktop": "Stationär",
|
||||
"metrics.device.laptop": "Bärbar",
|
||||
"metrics.device.mobile": "Mobil",
|
||||
"metrics.device.tablet": "Platta",
|
||||
"metrics.devices": "Enheter",
|
||||
|
@ -84,14 +95,5 @@
|
|||
"metrics.referrers": "Hänvisare",
|
||||
"metrics.unique-visitors": "Unika besökare",
|
||||
"metrics.views": "Visningar",
|
||||
"metrics.visitors": "Besökare",
|
||||
"title.add-account": "Lägg till konto",
|
||||
"title.add-website": "Lägg till webbsajt",
|
||||
"title.change-password": "Byt lösenord",
|
||||
"title.delete-account": "Radera konto",
|
||||
"title.delete-website": "Radera webbsajt",
|
||||
"title.edit-account": "Redigera konto",
|
||||
"title.edit-website": "Redigera webbsajt",
|
||||
"title.share-url": "Delnings-URL",
|
||||
"title.tracking-code": "Spårningskod"
|
||||
"metrics.visitors": "Besökare"
|
||||
}
|
||||
|
|
|
@ -1,50 +1,60 @@
|
|||
{
|
||||
"button.add-account": "Yeni Hesap Ekle",
|
||||
"button.add-website": "Web sitesi ekle",
|
||||
"button.back": "Geri",
|
||||
"button.cancel": "İptal",
|
||||
"button.change-password": "Şifre değiştir",
|
||||
"button.copy-to-clipboard": "Panoya kopyala",
|
||||
"button.date-range": "Tarih aralığı",
|
||||
"button.delete": "Sil",
|
||||
"button.dismiss": "Dismiss",
|
||||
"button.edit": "Düzenle",
|
||||
"button.login": "Giriş Yap",
|
||||
"button.more": "Detaylı göster",
|
||||
"button.refresh": "Yenile",
|
||||
"button.reset": "Sıfırla",
|
||||
"button.save": "Kaydet",
|
||||
"button.single-day": "Tekil gün",
|
||||
"button.view-details": "Detayı incele",
|
||||
"label.accounts": "Hesaplar",
|
||||
"label.add-account": "Hesap ekle",
|
||||
"label.add-website": "Web sitesi ekle",
|
||||
"label.administrator": "Yönetici",
|
||||
"label.all": "Tümü",
|
||||
"label.all-websites": "Tüm web siteleri",
|
||||
"label.back": "Geri",
|
||||
"label.cancel": "İptal",
|
||||
"label.change-password": "Şifre değiştir",
|
||||
"label.confirm-password": "Parolayı onayla",
|
||||
"label.copy-to-clipboard": "Panoya kopyala",
|
||||
"label.current-password": "Mevcut parola",
|
||||
"label.custom-range": "Özelleştirilmiş aralık",
|
||||
"label.dashboard": "Kontrol Paneli",
|
||||
"label.date-range": "Tarih aralığı",
|
||||
"label.default-date-range": "Varsayılan tarih aralığı",
|
||||
"label.delete": "Sil",
|
||||
"label.delete-account": "Hesabı sil",
|
||||
"label.delete-website": "Web sitesini sil",
|
||||
"label.dismiss": "Reddet",
|
||||
"label.domain": "Alan adı",
|
||||
"label.edit": "Düzenle",
|
||||
"label.edit-account": "Hesabı düzenle",
|
||||
"label.edit-website": "Web sitesini düzenle",
|
||||
"label.enable-share-url": "Anonim paylaşım URL'i aktif",
|
||||
"label.invalid": "Geçeriz",
|
||||
"label.invalid": "Geçersiz",
|
||||
"label.invalid-domain": "Geçersiz alan adı",
|
||||
"label.last-days": "Son {x} gün",
|
||||
"label.last-hours": "Son {x} saat",
|
||||
"label.logged-in-as": "{username} olarak giriş yapıldı.",
|
||||
"label.login": "Giriş Yap",
|
||||
"label.logout": "Çıkış Yap",
|
||||
"label.more": "Detaylı göster",
|
||||
"label.name": "İsim",
|
||||
"label.new-password": "Yeni parola",
|
||||
"label.password": "Parola",
|
||||
"label.passwords-dont-match": "Parolalar uyuşmuyor",
|
||||
"label.profile": "Profil",
|
||||
"label.realtime": "Gerçek Zamanlı",
|
||||
"label.realtime-logs": "Gerçek zamanlı kayıtlar",
|
||||
"label.refresh": "Yenile",
|
||||
"label.required": "Zorunlu alan",
|
||||
"label.reset": "Sıfırla",
|
||||
"label.save": "Kaydet",
|
||||
"label.settings": "Ayarlar",
|
||||
"label.share-url": "Paylaşım adresi",
|
||||
"label.single-day": "Tekil gün",
|
||||
"label.this-month": "Bu ay",
|
||||
"label.this-week": "Bu hafta",
|
||||
"label.this-year": "Bu yıl",
|
||||
"label.timezone": "Zaman dilimi",
|
||||
"label.today": "Bugün",
|
||||
"label.tracking-code": "İzleme kodu",
|
||||
"label.unknown": "Bilinmeyen",
|
||||
"label.username": "Kullanıcı adı",
|
||||
"label.view-details": "Detayı incele",
|
||||
"label.websites": "Web siteleri",
|
||||
"message.active-users": "{x} aktif ziyaretçi",
|
||||
"message.confirm-delete": "{target} kaydını silmek istediğinizden emin misiniz?",
|
||||
|
@ -55,7 +65,8 @@
|
|||
"message.get-tracking-code": "İzleme kodunu al",
|
||||
"message.go-to-settings": "Ayarlara git",
|
||||
"message.incorrect-username-password": "Hatalı kullanıcı adı ya da parola.",
|
||||
"message.new-version-available": "A new version of umami {version} is available!",
|
||||
"message.log.visitor": "Yeni ziyaretçi: {country}, {os}, {device}, {browser}",
|
||||
"message.new-version-available": "umami'nin yeni bir versiyonu ({version}) mevcut!",
|
||||
"message.no-data-available": "Henüz hiç veri yok.",
|
||||
"message.no-websites-configured": "Henüz hiç web sitesi tanımlamadınız",
|
||||
"message.page-not-found": "Sayfa bulunamadı.",
|
||||
|
@ -84,14 +95,5 @@
|
|||
"metrics.referrers": "Yönlendirenler",
|
||||
"metrics.unique-visitors": "Tekil kullanıcı",
|
||||
"metrics.views": "Görüntüleme",
|
||||
"metrics.visitors": "Ziyaretçi",
|
||||
"title.add-account": "Hesap ekle",
|
||||
"title.add-website": "Web sitesi ekle",
|
||||
"title.change-password": "Şifre değiştir",
|
||||
"title.delete-account": "Hesabı sil",
|
||||
"title.delete-website": "Web sitesini sil",
|
||||
"title.edit-account": "Hesabı düzenle",
|
||||
"title.edit-website": "Web sitesini düzenle",
|
||||
"title.share-url": "Paylaşım adresi",
|
||||
"title.tracking-code": "İzleme kodu"
|
||||
}
|
||||
"metrics.visitors": "Ziyaretçi"
|
||||
}
|
|
@ -1,51 +1,61 @@
|
|||
{
|
||||
"button.add-account": "Додати обліковий запис",
|
||||
"button.add-website": "Додати веб-сайт",
|
||||
"button.back": "Назад",
|
||||
"button.cancel": "Відмінити",
|
||||
"button.change-password": "Змінити пароль",
|
||||
"button.copy-to-clipboard": "Копіювати до буферу обміну",
|
||||
"button.date-range": "Діапазон дат",
|
||||
"button.delete": "Видалити",
|
||||
"button.dismiss": "Відхилити",
|
||||
"button.edit": "Редагувати",
|
||||
"button.login": "Увійти",
|
||||
"button.more": "Більше",
|
||||
"button.refresh": "Оновити",
|
||||
"button.reset": "Скинути",
|
||||
"button.save": "Зберегти",
|
||||
"button.single-day": "Один день",
|
||||
"button.view-details": "Переглянути деталі",
|
||||
"label.accounts": "Облікові записи",
|
||||
"label.add-account": "Додати обліковий запис",
|
||||
"label.add-website": "Додати вебсайт",
|
||||
"label.administrator": "Адміністратор",
|
||||
"label.all": "Всі",
|
||||
"label.all-websites": "Всі вебсайти",
|
||||
"label.back": "Назад",
|
||||
"label.cancel": "Відмінити",
|
||||
"label.change-password": "Змінити пароль",
|
||||
"label.confirm-password": "Підтвердити пароль",
|
||||
"label.copy-to-clipboard": "Копіювати до буферу обміну",
|
||||
"label.current-password": "Поточний пароль",
|
||||
"label.custom-range": "Довільний період",
|
||||
"label.dashboard": "Інформаційна панель",
|
||||
"label.date-range": "Діапазон дат",
|
||||
"label.default-date-range": "Діапазон дат за умовчанням",
|
||||
"label.delete": "Видалити",
|
||||
"label.delete-account": "Видалити обліковий запис",
|
||||
"label.delete-website": "Видалити вебсайт",
|
||||
"label.dismiss": "Відхилити",
|
||||
"label.domain": "Домен",
|
||||
"label.edit": "Редагувати",
|
||||
"label.edit-account": "Редагувати обліковий запис",
|
||||
"label.edit-website": "Редагувати вебсайт",
|
||||
"label.enable-share-url": "Дозволити ділитися посиланням",
|
||||
"label.invalid": "Некоректний",
|
||||
"label.invalid-domain": "Некоректний домен",
|
||||
"label.last-days": "Останні {x} днів",
|
||||
"label.last-hours": "Останні {x} годин",
|
||||
"label.logged-in-as": "Ви увійшли як {username}",
|
||||
"label.login": "Увійти",
|
||||
"label.logout": "Вийти",
|
||||
"label.more": "Більше",
|
||||
"label.name": "Ім'я",
|
||||
"label.new-password": "Новий пароль",
|
||||
"label.password": "Пароль",
|
||||
"label.passwords-dont-match": "Паролі не співпадають",
|
||||
"label.profile": "Профіль",
|
||||
"label.realtime": "У реальному часі",
|
||||
"label.realtime-logs": "Логи у реальному часі",
|
||||
"label.refresh": "Оновити",
|
||||
"label.required": "Обов'язкове",
|
||||
"label.reset": "Скинути",
|
||||
"label.save": "Зберегти",
|
||||
"label.settings": "Налаштування",
|
||||
"label.share-url": "Поділитися посилання",
|
||||
"label.single-day": "Один день",
|
||||
"label.this-month": "Поточний місяць",
|
||||
"label.this-week": "Поточний тиждень",
|
||||
"label.this-year": "Поточний рік",
|
||||
"label.timezone": "Часовий пояс",
|
||||
"label.today": "Сьогодні",
|
||||
"label.tracking-code": "Код для відслідковування",
|
||||
"label.unknown": "Невідомо",
|
||||
"label.username": "Ім'я користувача",
|
||||
"label.websites": "Веб-сайти",
|
||||
"label.view-details": "Переглянути деталі",
|
||||
"label.websites": "Вебсайти",
|
||||
"message.active-users": "{x} поточних відвідувачів",
|
||||
"message.confirm-delete": "Ви впевнені, що бажаєте видалити {target}?",
|
||||
"message.copied": "Скопійовано!",
|
||||
|
@ -55,14 +65,15 @@
|
|||
"message.get-tracking-code": "Отримати код для відслідковування",
|
||||
"message.go-to-settings": "Перейти до налаштувань",
|
||||
"message.incorrect-username-password": "Невірне ім'я користувача або пароль.",
|
||||
"message.log.visitor": "Відвідувач з {country} використовуючи {browser} на {os} {device}",
|
||||
"message.new-version-available": "Нова версія umami {version} доступна!",
|
||||
"message.no-data-available": "Немає даних.",
|
||||
"message.no-websites-configured": "У вас немає налаштованих веб-сайтів.",
|
||||
"message.no-websites-configured": "У вас немає налаштованих вебсайтів.",
|
||||
"message.page-not-found": "Сторінку не знайдено.",
|
||||
"message.powered-by": "На базі {name}",
|
||||
"message.save-success": "Збережено успішно.",
|
||||
"message.share-url": "Це публічне посилання для {target}.",
|
||||
"message.track-stats": "Або відслідковувати статистику для {target}, розмістіть наступний код у {head} секції вашого веб-сайту.",
|
||||
"message.track-stats": "Або відслідковувати статистику для {target}, розмістіть наступний код у {head} секції вашого вебсайту.",
|
||||
"message.type-delete": "Введіть {delete} у полі нижче щоб підтвердити.",
|
||||
"metrics.actions": "Дії",
|
||||
"metrics.average-visit-time": "Середній час візиту",
|
||||
|
@ -84,14 +95,5 @@
|
|||
"metrics.referrers": "Джерела",
|
||||
"metrics.unique-visitors": "Унікальні відвідувачі",
|
||||
"metrics.views": "Перегляди",
|
||||
"metrics.visitors": "Відвідувачі",
|
||||
"title.add-account": "Додати обліковий запис",
|
||||
"title.add-website": "Додати website",
|
||||
"title.change-password": "Змінити пароль",
|
||||
"title.delete-account": "Видалити обліковий запис",
|
||||
"title.delete-website": "Видалити веб-сайт",
|
||||
"title.edit-account": "Редагувати обліковий запис",
|
||||
"title.edit-website": "Редагувати веб-сайт",
|
||||
"title.share-url": "Поділитися посилання",
|
||||
"title.tracking-code": "Код для відслідковування"
|
||||
"metrics.visitors": "Відвідувачі"
|
||||
}
|
||||
|
|
|
@ -1,50 +1,60 @@
|
|||
{
|
||||
"button.add-account": "添加账户",
|
||||
"button.add-website": "添加网站",
|
||||
"button.back": "返回",
|
||||
"button.cancel": "取消",
|
||||
"button.change-password": "更新密码",
|
||||
"button.copy-to-clipboard": "复制",
|
||||
"button.date-range": "多日",
|
||||
"button.delete": "删除",
|
||||
"button.dismiss": "Dismiss",
|
||||
"button.edit": "编辑",
|
||||
"button.login": "登录",
|
||||
"button.more": "更多",
|
||||
"button.refresh": "刷新",
|
||||
"button.reset": "重置",
|
||||
"button.save": "保存",
|
||||
"button.single-day": "单日",
|
||||
"button.view-details": "查看更多",
|
||||
"label.accounts": "账户",
|
||||
"label.add-account": "添加账户",
|
||||
"label.add-website": "添加网站",
|
||||
"label.administrator": "管理员",
|
||||
"label.all": "所有",
|
||||
"label.all-websites": "全部网站",
|
||||
"label.back": "返回",
|
||||
"label.cancel": "取消",
|
||||
"label.change-password": "更新密码",
|
||||
"label.confirm-password": "确认密码",
|
||||
"label.copy-to-clipboard": "复制",
|
||||
"label.current-password": "目前密码",
|
||||
"label.custom-range": "自定义时间段",
|
||||
"label.dashboard": "仪表板",
|
||||
"label.date-range": "多日",
|
||||
"label.default-date-range": "默认日期范围",
|
||||
"label.delete": "删除",
|
||||
"label.delete-account": "删除账户",
|
||||
"label.delete-website": "删除网站",
|
||||
"label.dismiss": "关闭",
|
||||
"label.domain": "域名",
|
||||
"label.edit": "编辑",
|
||||
"label.edit-account": "编辑账户",
|
||||
"label.edit-website": "编辑网站",
|
||||
"label.enable-share-url": "激活共享链接",
|
||||
"label.invalid": "输入无效",
|
||||
"label.invalid-domain": "无效域名",
|
||||
"label.last-days": "最近 {x} 天",
|
||||
"label.last-hours": "最近 {x} 小时",
|
||||
"label.logged-in-as": "登录名: {username}",
|
||||
"label.login": "登录",
|
||||
"label.logout": "退出",
|
||||
"label.more": "更多",
|
||||
"label.name": "名字",
|
||||
"label.new-password": "新密码",
|
||||
"label.password": "密码",
|
||||
"label.passwords-dont-match": "密码不一致",
|
||||
"label.profile": "个人资料",
|
||||
"label.realtime": "实时",
|
||||
"label.realtime-logs": "实时日志",
|
||||
"label.refresh": "刷新",
|
||||
"label.required": "必填",
|
||||
"label.reset": "重置",
|
||||
"label.save": "保存",
|
||||
"label.settings": "设置",
|
||||
"label.share-url": "共享链接",
|
||||
"label.single-day": "单日",
|
||||
"label.this-month": "本月",
|
||||
"label.this-week": "本周",
|
||||
"label.this-year": "今年",
|
||||
"label.timezone": "时区",
|
||||
"label.today": "今天",
|
||||
"label.tracking-code": "跟踪代码",
|
||||
"label.unknown": "未知",
|
||||
"label.username": "用户名",
|
||||
"label.view-details": "查看更多",
|
||||
"label.websites": "网站",
|
||||
"message.active-users": "当前在线 {x} 人",
|
||||
"message.confirm-delete": "你确定要删除{target}吗?",
|
||||
|
@ -55,7 +65,8 @@
|
|||
"message.get-tracking-code": "获得跟踪代码",
|
||||
"message.go-to-settings": "去设置",
|
||||
"message.incorrect-username-password": "用户名密码不正确.",
|
||||
"message.new-version-available": "A new version of umami {version} is available!",
|
||||
"message.log.visitor": "来自 {country} 的访客在搭载 {os} 的 {device} 上使用 {browser} 进行访问.",
|
||||
"message.new-version-available": "umami 有新版本 {version} 发布啦!",
|
||||
"message.no-data-available": "无可用数据.",
|
||||
"message.no-websites-configured": "你还没有设置任何网站.",
|
||||
"message.page-not-found": "网页未找到.",
|
||||
|
@ -84,14 +95,5 @@
|
|||
"metrics.referrers": "指入域名",
|
||||
"metrics.unique-visitors": "独立访客",
|
||||
"metrics.views": "页面流量",
|
||||
"metrics.visitors": "独立访客",
|
||||
"title.add-account": "添加账户",
|
||||
"title.add-website": "添加网站",
|
||||
"title.change-password": "更新密码",
|
||||
"title.delete-account": "删除账户",
|
||||
"title.delete-website": "删除网站",
|
||||
"title.edit-account": "编辑账户",
|
||||
"title.edit-website": "编辑网站",
|
||||
"title.share-url": "共享链接",
|
||||
"title.tracking-code": "跟踪代码"
|
||||
"metrics.visitors": "独立访客"
|
||||
}
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue