Merge pull request #296 from mikecao/dev

v0.96.0 Realtime dashboard
This commit is contained in:
Mike Cao 2020-10-13 18:47:35 -07:00 committed by GitHub
commit f13d7cfed5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
109 changed files with 2377 additions and 1201 deletions

1
assets/bolt.svg Normal file
View File

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

1
assets/eye.svg Normal file
View File

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

1
assets/visitor.svg Normal file
View File

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

View File

@ -3,7 +3,7 @@ import Button from './Button';
import { FormattedMessage } from 'react-intl'; import { FormattedMessage } from 'react-intl';
const defaultText = ( 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 }) { export default function CopyButton({ element, ...props }) {

15
components/common/Dot.js Normal file
View File

@ -0,0 +1,15 @@
import React from 'react';
import classNames from 'classnames';
import styles from './Dot.module.css';
export default function Dot({ color, size, className }) {
return (
<div
style={{ background: color }}
className={classNames(styles.dot, className, {
[styles.small]: size === 'small',
[styles.large]: size === 'large',
})}
/>
);
}

View File

@ -0,0 +1,17 @@
.dot {
background: var(--green400);
width: 10px;
height: 10px;
border-radius: 100%;
margin-right: 10px;
}
.dot.small {
width: 8px;
height: 8px;
}
.dot.large {
width: 16px;
height: 16px;
}

View File

@ -6,7 +6,7 @@ import styles from './EmptyPlaceholder.module.css';
export default function EmptyPlaceholder({ msg, children }) { export default function EmptyPlaceholder({ msg, children }) {
return ( return (
<div className={styles.placeholder}> <div className={styles.placeholder}>
<Icon icon={<Logo />} size="xlarge" /> <Icon className={styles.icon} icon={<Logo />} size="xlarge" />
<h2>{msg}</h2> <h2>{msg}</h2>
{children} {children}
</div> </div>

View File

@ -5,3 +5,7 @@
align-items: center; align-items: center;
min-height: 600px; min-height: 600px;
} }
.icon {
margin-bottom: 30px;
}

View File

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

View File

@ -12,7 +12,7 @@ export default function RefreshButton({ websiteId }) {
const dispatch = useDispatch(); const dispatch = useDispatch();
const [dateRange] = useDateRange(websiteId); const [dateRange] = useDateRange(websiteId);
const [loading, setLoading] = useState(false); 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() { function handleClick() {
if (dateRange) { if (dateRange) {
@ -28,7 +28,7 @@ export default function RefreshButton({ websiteId }) {
return ( return (
<Button <Button
icon={loading ? <Dots /> : <Refresh />} icon={loading ? <Dots /> : <Refresh />}
tooltip={<FormattedMessage id="button.refresh" defaultMessage="Refresh" />} tooltip={<FormattedMessage id="label.refresh" defaultMessage="Refresh" />}
tooltipId="button-refresh" tooltipId="button-refresh"
size="small" size="small"
onClick={handleClick} onClick={handleClick}

View File

@ -3,40 +3,58 @@ import classNames from 'classnames';
import NoData from 'components/common/NoData'; import NoData from 'components/common/NoData';
import styles from './Table.module.css'; 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) { if (empty && rows.length === 0) {
return empty; return empty;
} }
return ( return (
<div className={styles.table}> <div className={classNames(styles.table, className)}>
<div className={classNames(styles.header, 'row')}> {showHeader && (
{columns.map(({ key, label, className, style, header }) => ( <div className={classNames(styles.header, 'row')}>
<div {columns.map(({ key, label, className, style, header }) => (
key={key} <div
className={classNames(styles.head, className, header?.className)} key={key}
style={{ ...style, ...header?.style }} className={classNames(styles.head, className, header?.className)}
> style={{ ...style, ...header?.style }}
{label} >
</div> {label}
))} </div>
</div> ))}
<div className={styles.body}> </div>
)}
<div className={classNames(styles.body, bodyClassName)}>
{rows.length === 0 && <NoData />} {rows.length === 0 && <NoData />}
{rows.map((row, rowIndex) => ( {!children &&
<div className={classNames(styles.row, 'row')} key={rowIndex}> rows.map((row, index) => {
{columns.map(({ key, render, className, style, cell }) => ( const id = rowKey ? rowKey(row) : index;
<div return <TableRow key={id} columns={columns} row={row} />;
key={`${rowIndex}${key}`} })}
className={classNames(styles.cell, className, cell?.className)} {children}
style={{ ...style, ...cell?.style }}
>
{render ? render(row) : row[key]}
</div>
))}
</div>
))}
</div> </div>
</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>
);

7
components/common/Tag.js Normal file
View File

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

View File

@ -1,5 +1,4 @@
.type { .tag {
font-size: var(--font-size-small);
padding: 2px 4px; padding: 2px 4px;
border: 1px solid var(--gray300); border: 1px solid var(--gray300);
border-radius: 4px; border-radius: 4px;

View File

@ -36,10 +36,10 @@ export default function UpdateNotice() {
</div> </div>
<ButtonLayout> <ButtonLayout>
<Button size="xsmall" variant="action" onClick={handleViewClick}> <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>
<Button size="xsmall" onClick={handleDismissClick}> <Button size="xsmall" onClick={handleDismissClick}>
<FormattedMessage id="button.dismiss" defaultMessage="Dismiss" /> <FormattedMessage id="label.dismiss" defaultMessage="Dismiss" />
</Button> </Button>
</ButtonLayout> </ButtonLayout>
</div> </div>

View File

@ -70,10 +70,10 @@ export default function AccountEditForm({ values, onSave, onClose }) {
</FormRow> </FormRow>
<FormButtons> <FormButtons>
<Button type="submit" variant="action"> <Button type="submit" variant="action">
<FormattedMessage id="button.save" defaultMessage="Save" /> <FormattedMessage id="label.save" defaultMessage="Save" />
</Button> </Button>
<Button onClick={onClose}> <Button onClick={onClose}>
<FormattedMessage id="button.cancel" defaultMessage="Cancel" /> <FormattedMessage id="label.cancel" defaultMessage="Cancel" />
</Button> </Button>
</FormButtons> </FormButtons>
<FormMessage>{message}</FormMessage> <FormMessage>{message}</FormMessage>

View File

@ -85,10 +85,10 @@ export default function ChangePasswordForm({ values, onSave, onClose }) {
</FormRow> </FormRow>
<FormButtons> <FormButtons>
<Button type="submit" variant="action"> <Button type="submit" variant="action">
<FormattedMessage id="button.save" defaultMessage="Save" /> <FormattedMessage id="label.save" defaultMessage="Save" />
</Button> </Button>
<Button onClick={onClose}> <Button onClick={onClose}>
<FormattedMessage id="button.cancel" defaultMessage="Cancel" /> <FormattedMessage id="label.cancel" defaultMessage="Cancel" />
</Button> </Button>
</FormButtons> </FormButtons>
<FormMessage>{message}</FormMessage> <FormMessage>{message}</FormMessage>

View File

@ -33,11 +33,11 @@ export default function DatePickerForm({
const buttons = [ const buttons = [
{ {
label: <FormattedMessage id="button.single-day" defaultMessage="Single day" />, label: <FormattedMessage id="label.single-day" defaultMessage="Single day" />,
value: FILTER_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, value: FILTER_RANGE,
}, },
]; ];
@ -72,10 +72,10 @@ export default function DatePickerForm({
</div> </div>
<FormButtons> <FormButtons>
<Button variant="action" onClick={handleSave} disabled={disabled}> <Button variant="action" onClick={handleSave} disabled={disabled}>
<FormattedMessage id="button.save" defaultMessage="Save" /> <FormattedMessage id="label.save" defaultMessage="Save" />
</Button> </Button>
<Button onClick={onClose}> <Button onClick={onClose}>
<FormattedMessage id="button.cancel" defaultMessage="Cancel" /> <FormattedMessage id="label.cancel" defaultMessage="Cancel" />
</Button> </Button>
</FormButtons> </FormButtons>
</div> </div>

View File

@ -82,10 +82,10 @@ export default function DeleteForm({ values, onSave, onClose }) {
variant="danger" variant="danger"
disabled={props.values.confirmation !== CONFIRMATION_WORD} disabled={props.values.confirmation !== CONFIRMATION_WORD}
> >
<FormattedMessage id="button.delete" defaultMessage="Delete" /> <FormattedMessage id="label.delete" defaultMessage="Delete" />
</Button> </Button>
<Button onClick={onClose}> <Button onClick={onClose}>
<FormattedMessage id="button.cancel" defaultMessage="Cancel" /> <FormattedMessage id="label.cancel" defaultMessage="Cancel" />
</Button> </Button>
</FormButtons> </FormButtons>
<FormMessage>{message}</FormMessage> <FormMessage>{message}</FormMessage>

View File

@ -83,7 +83,7 @@ export default function LoginForm() {
</FormRow> </FormRow>
<FormButtons> <FormButtons>
<Button type="submit" variant="action"> <Button type="submit" variant="action">
<FormattedMessage id="button.login" defaultMessage="Login" /> <FormattedMessage id="label.login" defaultMessage="Login" />
</Button> </Button>
</FormButtons> </FormButtons>
<FormMessage>{message}</FormMessage> <FormMessage>{message}</FormMessage>

View File

@ -30,7 +30,7 @@ export default function TrackingCodeForm({ values, onClose }) {
<FormButtons> <FormButtons>
<CopyButton type="submit" variant="action" element={ref} /> <CopyButton type="submit" variant="action" element={ref} />
<Button onClick={onClose}> <Button onClick={onClose}>
<FormattedMessage id="button.cancel" defaultMessage="Cancel" /> <FormattedMessage id="label.cancel" defaultMessage="Cancel" />
</Button> </Button>
</FormButtons> </FormButtons>
</FormLayout> </FormLayout>

View File

@ -29,7 +29,7 @@ export default function TrackingCodeForm({ values, onClose }) {
<FormButtons> <FormButtons>
<CopyButton type="submit" variant="action" element={ref} /> <CopyButton type="submit" variant="action" element={ref} />
<Button onClick={onClose}> <Button onClick={onClose}>
<FormattedMessage id="button.cancel" defaultMessage="Cancel" /> <FormattedMessage id="label.cancel" defaultMessage="Cancel" />
</Button> </Button>
</FormButtons> </FormButtons>
</FormLayout> </FormLayout>

View File

@ -91,10 +91,10 @@ export default function WebsiteEditForm({ values, onSave, onClose }) {
</FormRow> </FormRow>
<FormButtons> <FormButtons>
<Button type="submit" variant="action"> <Button type="submit" variant="action">
<FormattedMessage id="button.save" defaultMessage="Save" /> <FormattedMessage id="label.save" defaultMessage="Save" />
</Button> </Button>
<Button onClick={onClose}> <Button onClick={onClose}>
<FormattedMessage id="button.cancel" defaultMessage="Cancel" /> <FormattedMessage id="label.cancel" defaultMessage="Cancel" />
</Button> </Button>
</FormButtons> </FormButtons>
<FormMessage>{message}</FormMessage> <FormMessage>{message}</FormMessage>

View File

@ -24,7 +24,9 @@ export default function Footer() {
}} }}
/> />
</div> </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> </div>
</footer> </footer>
); );

View File

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

View File

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

View File

@ -30,6 +30,9 @@ export default function Header() {
<Link href="/dashboard"> <Link href="/dashboard">
<FormattedMessage id="label.dashboard" defaultMessage="Dashboard" /> <FormattedMessage id="label.dashboard" defaultMessage="Dashboard" />
</Link> </Link>
<Link href="/realtime">
<FormattedMessage id="label.realtime" defaultMessage="Realtime" />
</Link>
<Link href="/settings"> <Link href="/settings">
<FormattedMessage id="label.settings" defaultMessage="Settings" /> <FormattedMessage id="label.settings" defaultMessage="Settings" />
</Link> </Link>

View File

@ -1,11 +1,18 @@
import React, { useMemo } from 'react'; import React, { useMemo } from 'react';
import { FormattedMessage } from 'react-intl';
import classNames from 'classnames'; import classNames from 'classnames';
import useFetch from 'hooks/useFetch'; 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 styles from './ActiveUsers.module.css';
import { FormattedMessage } from 'react-intl';
export default function ActiveUsers({ websiteId, token, className }) { export default function ActiveUsers({ websiteId, className }) {
const { data } = useFetch(`/api/website/${websiteId}/active`, { token }, { interval: 60000 }); const shareToken = useShareToken();
const { data } = useFetch(`/api/website/${websiteId}/active`, {
interval: 60000,
headers: { [TOKEN_HEADER]: shareToken?.token },
});
const count = useMemo(() => { const count = useMemo(() => {
return data?.[0]?.x || 0; return data?.[0]?.x || 0;
}, [data]); }, [data]);
@ -16,7 +23,7 @@ export default function ActiveUsers({ websiteId, token, className }) {
return ( return (
<div className={classNames(styles.container, className)}> <div className={classNames(styles.container, className)}>
<div className={styles.dot} /> <Dot />
<div className={styles.text}> <div className={styles.text}>
<div> <div>
<FormattedMessage <FormattedMessage

View File

@ -12,11 +12,3 @@
font-weight: 600; font-weight: 600;
margin-right: 4px; margin-right: 4px;
} }
.dot {
background: var(--green400);
width: 10px;
height: 10px;
border-radius: 100%;
margin-right: 10px;
}

View File

@ -5,17 +5,17 @@ import ChartJS from 'chart.js';
import { formatLongNumber } from 'lib/format'; import { formatLongNumber } from 'lib/format';
import { dateFormat } from 'lib/lang'; import { dateFormat } from 'lib/lang';
import useLocale from 'hooks/useLocale'; import useLocale from 'hooks/useLocale';
import styles from './BarChart.module.css';
import useTheme from 'hooks/useTheme'; 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';
export default function BarChart({ export default function BarChart({
chartId, chartId,
datasets, datasets,
unit, unit,
records, records,
height = 400, height = DEFAUL_CHART_HEIGHT,
animationDuration = 300, animationDuration = DEFAULT_ANIMATION_DURATION,
className, className,
stacked = false, stacked = false,
loading = false, loading = false,
@ -39,6 +39,8 @@ export default function BarChart({
const w = canvas.current.width; const w = canvas.current.width;
switch (unit) { switch (unit) {
case 'minute':
return index % 2 === 0 ? dateFormat(d, 'h:mm', locale) : '';
case 'hour': case 'hour':
return dateFormat(d, 'ha', locale); return dateFormat(d, 'ha', locale);
case 'day': case 'day':
@ -63,7 +65,7 @@ export default function BarChart({
} }
function renderYLabel(label) { function renderYLabel(label) {
return +label > 1 ? formatLongNumber(label) : label; return +label > 1000 ? formatLongNumber(label) : label;
} }
function renderTooltip(model) { function renderTooltip(model) {

View File

@ -3,15 +3,14 @@ import { FormattedMessage } from 'react-intl';
import MetricsTable from './MetricsTable'; import MetricsTable from './MetricsTable';
import { browserFilter } from 'lib/filters'; import { browserFilter } from 'lib/filters';
export default function BrowsersTable({ websiteId, token, limit }) { export default function BrowsersTable({ websiteId, ...props }) {
return ( return (
<MetricsTable <MetricsTable
{...props}
title={<FormattedMessage id="metrics.browsers" defaultMessage="Browsers" />} title={<FormattedMessage id="metrics.browsers" defaultMessage="Browsers" />}
type="browser" type="browser"
metric={<FormattedMessage id="metrics.visitors" defaultMessage="Visitors" />} metric={<FormattedMessage id="metrics.visitors" defaultMessage="Visitors" />}
websiteId={websiteId} websiteId={websiteId}
token={token}
limit={limit}
dataFilter={browserFilter} dataFilter={browserFilter}
/> />
); );

View File

@ -5,7 +5,7 @@ import { FormattedMessage } from 'react-intl';
import useCountryNames from 'hooks/useCountryNames'; import useCountryNames from 'hooks/useCountryNames';
import useLocale from 'hooks/useLocale'; import useLocale from 'hooks/useLocale';
export default function CountriesTable({ websiteId, token, limit, onDataLoad = () => {} }) { export default function CountriesTable({ websiteId, onDataLoad, ...props }) {
const [locale] = useLocale(); const [locale] = useLocale();
const countryNames = useCountryNames(locale); const countryNames = useCountryNames(locale);
@ -15,13 +15,12 @@ export default function CountriesTable({ websiteId, token, limit, onDataLoad = (
return ( return (
<MetricsTable <MetricsTable
{...props}
title={<FormattedMessage id="metrics.countries" defaultMessage="Countries" />} title={<FormattedMessage id="metrics.countries" defaultMessage="Countries" />}
type="country" type="country"
metric={<FormattedMessage id="metrics.visitors" defaultMessage="Visitors" />} metric={<FormattedMessage id="metrics.visitors" defaultMessage="Visitors" />}
websiteId={websiteId} websiteId={websiteId}
token={token} onDataLoad={data => onDataLoad?.(percentFilter(data))}
limit={limit}
onDataLoad={data => onDataLoad(percentFilter(data))}
renderLabel={renderLabel} renderLabel={renderLabel}
/> />
); );

View File

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

View File

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

View File

@ -4,15 +4,14 @@ import { deviceFilter } from 'lib/filters';
import { FormattedMessage } from 'react-intl'; import { FormattedMessage } from 'react-intl';
import { getDeviceMessage } from 'components/messages'; import { getDeviceMessage } from 'components/messages';
export default function DevicesTable({ websiteId, token, limit }) { export default function DevicesTable({ websiteId, ...props }) {
return ( return (
<MetricsTable <MetricsTable
{...props}
title={<FormattedMessage id="metrics.devices" defaultMessage="Devices" />} title={<FormattedMessage id="metrics.devices" defaultMessage="Devices" />}
type="device" type="device"
metric={<FormattedMessage id="metrics.visitors" defaultMessage="Visitors" />} metric={<FormattedMessage id="metrics.visitors" defaultMessage="Visitors" />}
websiteId={websiteId} websiteId={websiteId}
token={token}
limit={limit}
dataFilter={deviceFilter} dataFilter={deviceFilter}
renderLabel={({ x }) => getDeviceMessage(x)} renderLabel={({ x }) => getDeviceMessage(x)}
/> />

View File

@ -5,26 +5,31 @@ import { getDateArray, getDateLength } from 'lib/date';
import useFetch from 'hooks/useFetch'; import useFetch from 'hooks/useFetch';
import useDateRange from 'hooks/useDateRange'; import useDateRange from 'hooks/useDateRange';
import useTimezone from 'hooks/useTimezone'; 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 [dateRange] = useDateRange(websiteId);
const { startDate, endDate, unit, modified } = dateRange; const { startDate, endDate, unit, modified } = dateRange;
const [timezone] = useTimezone(); const [timezone] = useTimezone();
const { query } = usePageQuery(); const { query } = usePageQuery();
const shareToken = useShareToken();
const { data } = useFetch( const { data } = useFetch(
`/api/website/${websiteId}/events`, `/api/website/${websiteId}/events`,
{ {
start_at: +startDate, params: {
end_at: +endDate, start_at: +startDate,
unit, end_at: +endDate,
tz: timezone, unit,
url: query.url, tz: timezone,
token, url: query.url,
token,
},
headers: { [TOKEN_HEADER]: shareToken?.token },
}, },
{ update: [modified] }, [modified],
); );
const datasets = useMemo(() => { const datasets = useMemo(() => {
if (!data) return []; if (!data) return [];
@ -44,7 +49,7 @@ export default function EventsChart({ websiteId, token }) {
}); });
return Object.keys(map).map((key, index) => { return Object.keys(map).map((key, index) => {
const color = tinycolor(EVENT_COLORS[index]); const color = tinycolor(EVENT_COLORS[index % EVENT_COLORS.length]);
return { return {
label: key, label: key,
data: map[key], data: map[key],
@ -77,6 +82,7 @@ export default function EventsChart({ websiteId, token }) {
return ( return (
<BarChart <BarChart
chartId={`events-${websiteId}`} chartId={`events-${websiteId}`}
className={className}
datasets={datasets} datasets={datasets}
unit={unit} unit={unit}
records={getDateLength(startDate, endDate, unit)} records={getDateLength(startDate, endDate, unit)}

View File

@ -1,19 +1,17 @@
import React from 'react'; import React from 'react';
import { FormattedMessage } from 'react-intl'; import { FormattedMessage } from 'react-intl';
import MetricsTable from './MetricsTable'; 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 ( return (
<MetricsTable <MetricsTable
{...props}
title={<FormattedMessage id="metrics.events" defaultMessage="Events" />} title={<FormattedMessage id="metrics.events" defaultMessage="Events" />}
type="event" type="event"
metric={<FormattedMessage id="metrics.actions" defaultMessage="Actions" />} metric={<FormattedMessage id="metrics.actions" defaultMessage="Actions" />}
websiteId={websiteId} websiteId={websiteId}
token={token}
limit={limit}
renderLabel={({ x }) => <Label value={x} />} renderLabel={({ x }) => <Label value={x} />}
onDataLoad={onDataLoad}
/> />
); );
} }
@ -22,7 +20,7 @@ const Label = ({ value }) => {
const [event, label] = value.split(':'); const [event, label] = value.split(':');
return ( return (
<> <>
<span className={styles.type}>{event}</span> <Tag>{event}</Tag>
{label} {label}
</> </>
); );

View File

@ -5,12 +5,15 @@ import Loading from 'components/common/Loading';
import ErrorMessage from 'components/common/ErrorMessage'; import ErrorMessage from 'components/common/ErrorMessage';
import useFetch from 'hooks/useFetch'; import useFetch from 'hooks/useFetch';
import useDateRange from 'hooks/useDateRange'; import useDateRange from 'hooks/useDateRange';
import { formatShortTime, formatNumber, formatLongNumber } from 'lib/format';
import usePageQuery from 'hooks/usePageQuery'; 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 MetricCard from './MetricCard';
import styles from './MetricsBar.module.css'; 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 [dateRange] = useDateRange(websiteId);
const { startDate, endDate, modified } = dateRange; const { startDate, endDate, modified } = dateRange;
const [format, setFormat] = useState(true); const [format, setFormat] = useState(true);
@ -19,16 +22,16 @@ export default function MetricsBar({ websiteId, token, className }) {
} = usePageQuery(); } = usePageQuery();
const { data, error, loading } = useFetch( const { data, error, loading } = useFetch(
`/api/website/${websiteId}/metrics`, `/api/website/${websiteId}/stats`,
{ {
start_at: +startDate, params: {
end_at: +endDate, start_at: +startDate,
url, end_at: +endDate,
token, url,
}, },
{ headers: { [TOKEN_HEADER]: shareToken?.token },
update: [modified],
}, },
[modified],
); );
const formatFunc = format ? formatLongNumber : formatNumber; const formatFunc = format ? formatLongNumber : formatNumber;

View File

@ -1,34 +1,31 @@
import React, { useState, useMemo } from 'react'; import React, { useMemo } from 'react';
import { FormattedMessage } from 'react-intl'; import { FormattedMessage } from 'react-intl';
import { FixedSizeList } from 'react-window';
import { useSpring, animated, config } from 'react-spring';
import classNames from 'classnames'; import classNames from 'classnames';
import Link from 'components/common/Link'; import Link from 'components/common/Link';
import Loading from 'components/common/Loading'; import Loading from 'components/common/Loading';
import NoData from 'components/common/NoData';
import useFetch from 'hooks/useFetch'; import useFetch from 'hooks/useFetch';
import Arrow from 'assets/arrow-right.svg'; import Arrow from 'assets/arrow-right.svg';
import { percentFilter } from 'lib/filters'; import { percentFilter } from 'lib/filters';
import { formatNumber, formatLongNumber } from 'lib/format';
import useDateRange from 'hooks/useDateRange'; import useDateRange from 'hooks/useDateRange';
import usePageQuery from 'hooks/usePageQuery'; 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 styles from './MetricsTable.module.css';
import ErrorMessage from '../common/ErrorMessage';
export default function MetricsTable({ export default function MetricsTable({
websiteId, websiteId,
websiteDomain, websiteDomain,
token,
title,
metric,
type, type,
className, className,
dataFilter, dataFilter,
filterOptions, filterOptions,
limit, limit,
renderLabel, onDataLoad,
onDataLoad = () => {}, ...props
}) { }) {
const shareToken = useShareToken();
const [dateRange] = useDateRange(websiteId); const [dateRange] = useDateRange(websiteId);
const { startDate, endDate, modified } = dateRange; const { startDate, endDate, modified } = dateRange;
const { const {
@ -38,22 +35,23 @@ export default function MetricsTable({
} = usePageQuery(); } = usePageQuery();
const { data, loading, error } = useFetch( const { data, loading, error } = useFetch(
`/api/website/${websiteId}/rankings`, `/api/website/${websiteId}/metrics`,
{ {
type, params: {
start_at: +startDate, type,
end_at: +endDate, start_at: +startDate,
domain: websiteDomain, end_at: +endDate,
url, domain: websiteDomain,
token, 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) { if (data) {
const items = percentFilter(dataFilter ? dataFilter(data, filterOptions) : data); const items = percentFilter(dataFilter ? dataFilter(data, filterOptions) : data);
if (limit) { if (limit) {
@ -64,91 +62,24 @@ export default function MetricsTable({
return []; return [];
}, [data, error, dataFilter, filterOptions]); }, [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 ( return (
<div className={classNames(styles.container, className)}> <div className={classNames(styles.container, className)}>
{!data && loading && <Loading />} {!data && loading && <Loading />}
{error && <ErrorMessage />} {error && <ErrorMessage />}
{data && !error && ( {data && !error && <DataTable {...props} data={filteredData} className={className} />}
<> <div className={styles.footer}>
<div className={styles.header}> {data && !error && limit && (
<div className={styles.title}>{title}</div> <Link
<div className={styles.metric} onClick={handleSetFormat}> icon={<Arrow />}
{metric} href={router.pathname}
</div> as={resolve({ view: type })}
</div> size="small"
<div className={styles.body}> iconRight
{rankings?.length === 0 && <NoData />} >
{limit <FormattedMessage id="label.more" defaultMessage="More" />
? rankings.map(row => getRow(row)) </Link>
: rankings.length > 0 && ( )}
<FixedSizeList height={500} itemCount={rankings.length} itemSize={30}> </div>
{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>
</>
)}
</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>
);
};

View File

@ -6,95 +6,6 @@
flex-direction: column; 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 { .footer {
display: flex; display: flex;
justify-content: center; justify-content: center;

View File

@ -3,15 +3,14 @@ import MetricsTable from './MetricsTable';
import { osFilter } from 'lib/filters'; import { osFilter } from 'lib/filters';
import { FormattedMessage } from 'react-intl'; import { FormattedMessage } from 'react-intl';
export default function OSTable({ websiteId, token, limit }) { export default function OSTable({ websiteId, ...props }) {
return ( return (
<MetricsTable <MetricsTable
{...props}
title={<FormattedMessage id="metrics.operating-systems" defaultMessage="Operating system" />} title={<FormattedMessage id="metrics.operating-systems" defaultMessage="Operating system" />}
type="os" type="os"
metric={<FormattedMessage id="metrics.visitors" defaultMessage="Visitors" />} metric={<FormattedMessage id="metrics.visitors" defaultMessage="Visitors" />}
websiteId={websiteId} websiteId={websiteId}
token={token}
limit={limit}
dataFilter={osFilter} dataFilter={osFilter}
/> />
); );

View File

@ -2,15 +2,16 @@ import React, { useState } from 'react';
import { FormattedMessage } from 'react-intl'; import { FormattedMessage } from 'react-intl';
import classNames from 'classnames'; import classNames from 'classnames';
import Link from 'next/link'; import Link from 'next/link';
import ButtonGroup from 'components/common/ButtonGroup'; import FilterButtons from 'components/common/FilterButtons';
import ButtonLayout from 'components/layout/ButtonLayout';
import { urlFilter } from 'lib/filters'; import { urlFilter } from 'lib/filters';
import { FILTER_COMBINED, FILTER_RAW } from 'lib/constants';
import usePageQuery from 'hooks/usePageQuery'; import usePageQuery from 'hooks/usePageQuery';
import MetricsTable from './MetricsTable'; import MetricsTable from './MetricsTable';
import styles from './PagesTable.module.css'; 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 [filter, setFilter] = useState(FILTER_COMBINED);
const { const {
resolve, resolve,
@ -48,20 +49,11 @@ export default function PagesTable({ websiteId, token, websiteDomain, limit, sho
type="url" type="url"
metric={<FormattedMessage id="metrics.views" defaultMessage="Views" />} metric={<FormattedMessage id="metrics.views" defaultMessage="Views" />}
websiteId={websiteId} websiteId={websiteId}
token={token}
limit={limit}
dataFilter={urlFilter} dataFilter={urlFilter}
filterOptions={{ domain: websiteDomain, raw: filter === FILTER_RAW }} filterOptions={{ domain: websiteDomain, raw: filter === FILTER_RAW }}
renderLabel={renderLink} renderLabel={renderLink}
{...props}
/> />
</> </>
); );
} }
const FilterButtons = ({ buttons, selected, onClick }) => {
return (
<ButtonLayout>
<ButtonGroup size="xsmall" items={buttons} selectedItem={selected} onClick={onClick} />
</ButtonLayout>
);
};

View File

@ -4,9 +4,18 @@ import tinycolor from 'tinycolor2';
import CheckVisible from 'components/helpers/CheckVisible'; import CheckVisible from 'components/helpers/CheckVisible';
import BarChart from './BarChart'; import BarChart from './BarChart';
import useTheme from 'hooks/useTheme'; 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 intl = useIntl();
const [theme] = useTheme(); const [theme] = useTheme();
const primaryColor = tinycolor(THEME_COLORS[theme].primary); const primaryColor = tinycolor(THEME_COLORS[theme].primary);
@ -26,7 +35,7 @@ export default function PageviewsChart({ websiteId, data, unit, records, classNa
data: { datasets }, data: { datasets },
} = chart; } = chart;
datasets[0].data = data.uniques; datasets[0].data = data.sessions;
datasets[0].label = intl.formatMessage({ datasets[0].label = intl.formatMessage({
id: 'metrics.unique-visitors', id: 'metrics.unique-visitors',
defaultMessage: 'Unique visitors', defaultMessage: 'Unique visitors',
@ -48,6 +57,7 @@ export default function PageviewsChart({ websiteId, data, unit, records, classNa
<CheckVisible> <CheckVisible>
{visible => ( {visible => (
<BarChart <BarChart
{...props}
className={className} className={className}
chartId={websiteId} chartId={websiteId}
datasets={[ datasets={[
@ -56,7 +66,7 @@ export default function PageviewsChart({ websiteId, data, unit, records, classNa
id: 'metrics.unique-visitors', id: 'metrics.unique-visitors',
defaultMessage: 'Unique visitors', defaultMessage: 'Unique visitors',
}), }),
data: data.uniques, data: data.sessions,
lineTension: 0, lineTension: 0,
backgroundColor: colors.visitors.background, backgroundColor: colors.visitors.background,
borderColor: colors.visitors.border, borderColor: colors.visitors.border,
@ -76,7 +86,7 @@ export default function PageviewsChart({ websiteId, data, unit, records, classNa
]} ]}
unit={unit} unit={unit}
records={records} records={records}
animationDuration={visible ? 300 : 0} animationDuration={visible ? animationDuration : 0}
onUpdate={handleUpdate} onUpdate={handleUpdate}
loading={loading} loading={loading}
/> />

View File

@ -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}
/>
);
}

View File

@ -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>
</>
);
}

View File

@ -0,0 +1,3 @@
.metrics {
display: flex;
}

View File

@ -0,0 +1,168 @@
import React, { useMemo, useState } from 'react';
import { FormattedMessage } 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 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';
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 [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,
}) {
if (event_type) {
return (
<div>
<Tag>{event_type}</Tag> {event_value}
</div>
);
}
if (view_id) {
return url;
}
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: BROWSERS[browser],
os,
device,
}}
/>
);
}
}
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}>
<FixedSizeList height={400} itemCount={logs.length} itemSize={40}>
{Row}
</FixedSizeList>
</div>
</div>
);
}

View File

@ -0,0 +1,46 @@
.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;
}

View File

@ -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}
/>
)}
</>
);
}

View File

@ -1,12 +1,14 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import { FormattedMessage } from 'react-intl'; import { FormattedMessage } from 'react-intl';
import MetricsTable from './MetricsTable'; import MetricsTable from './MetricsTable';
import FilterButtons from 'components/common/FilterButtons';
import { refFilter } from 'lib/filters'; 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 [filter, setFilter] = useState(FILTER_COMBINED);
const buttons = [ const buttons = [
@ -35,13 +37,12 @@ export default function ReferrersTable({ websiteId, websiteDomain, token, limit,
<> <>
{showFilters && <FilterButtons buttons={buttons} selected={filter} onClick={setFilter} />} {showFilters && <FilterButtons buttons={buttons} selected={filter} onClick={setFilter} />}
<MetricsTable <MetricsTable
{...props}
title={<FormattedMessage id="metrics.referrers" defaultMessage="Referrers" />} title={<FormattedMessage id="metrics.referrers" defaultMessage="Referrers" />}
type="referrer" type="referrer"
metric={<FormattedMessage id="metrics.views" defaultMessage="Views" />} metric={<FormattedMessage id="metrics.views" defaultMessage="Views" />}
websiteId={websiteId} websiteId={websiteId}
websiteDomain={websiteDomain} websiteDomain={websiteDomain}
token={token}
limit={limit}
dataFilter={refFilter} dataFilter={refFilter}
filterOptions={{ filterOptions={{
domain: websiteDomain, 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>
);
};

View File

@ -14,15 +14,17 @@ import { getDateArray, getDateLength } from 'lib/date';
import Times from 'assets/times.svg'; import Times from 'assets/times.svg';
import styles from './WebsiteChart.module.css'; import styles from './WebsiteChart.module.css';
import ErrorMessage from '../common/ErrorMessage'; import ErrorMessage from '../common/ErrorMessage';
import useShareToken from '../../hooks/useShareToken';
import { TOKEN_HEADER } from '../../lib/constants';
export default function WebsiteChart({ export default function WebsiteChart({
websiteId, websiteId,
token,
title, title,
stickyHeader = false, stickyHeader = false,
showLink = false, showLink = false,
onDataLoad = () => {}, onDataLoad = () => {},
}) { }) {
const shareToken = useShareToken();
const [dateRange, setDateRange] = useDateRange(websiteId); const [dateRange, setDateRange] = useDateRange(websiteId);
const { startDate, endDate, unit, value, modified } = dateRange; const { startDate, endDate, unit, value, modified } = dateRange;
const [timezone] = useTimezone(); const [timezone] = useTimezone();
@ -35,24 +37,27 @@ export default function WebsiteChart({
const { data, loading, error } = useFetch( const { data, loading, error } = useFetch(
`/api/website/${websiteId}/pageviews`, `/api/website/${websiteId}/pageviews`,
{ {
start_at: +startDate, params: {
end_at: +endDate, start_at: +startDate,
unit, end_at: +endDate,
tz: timezone, unit,
url, tz: timezone,
token, url,
},
onDataLoad,
headers: { [TOKEN_HEADER]: shareToken?.token },
}, },
{ onDataLoad, update: [modified] }, [modified],
); );
const [pageviews, uniques] = useMemo(() => { const chartData = useMemo(() => {
if (data) { if (data) {
return [ return {
getDateArray(data.pageviews, startDate, endDate, unit), pageviews: getDateArray(data.pageviews, startDate, endDate, unit),
getDateArray(data.uniques, startDate, endDate, unit), sessions: getDateArray(data.sessions, startDate, endDate, unit),
]; };
} }
return [[], []]; return { pageviews: [], sessions: [] };
}, [data]); }, [data]);
function handleCloseFilter() { function handleCloseFilter() {
@ -61,7 +66,7 @@ export default function WebsiteChart({
return ( return (
<div className={styles.container}> <div className={styles.container}>
<WebsiteHeader websiteId={websiteId} token={token} title={title} showLink={showLink} /> <WebsiteHeader websiteId={websiteId} title={title} showLink={showLink} />
<div className={classNames(styles.header, 'row')}> <div className={classNames(styles.header, 'row')}>
<StickyHeader <StickyHeader
className={classNames(styles.metrics, 'col row')} className={classNames(styles.metrics, 'col row')}
@ -70,7 +75,7 @@ export default function WebsiteChart({
> >
{url && <PageFilter url={url} onClick={handleCloseFilter} />} {url && <PageFilter url={url} onClick={handleCloseFilter} />}
<div className="col-12 col-lg-9"> <div className="col-12 col-lg-9">
<MetricsBar websiteId={websiteId} token={token} /> <MetricsBar websiteId={websiteId} />
</div> </div>
<div className={classNames(styles.filter, 'col-12 col-lg-3')}> <div className={classNames(styles.filter, 'col-12 col-lg-3')}>
<DateFilter <DateFilter
@ -87,7 +92,7 @@ export default function WebsiteChart({
{error && <ErrorMessage />} {error && <ErrorMessage />}
<PageviewsChart <PageviewsChart
websiteId={websiteId} websiteId={websiteId}
data={{ pageviews, uniques }} data={chartData}
unit={unit} unit={unit}
records={getDateLength(startDate, endDate, unit)} records={getDateLength(startDate, endDate, unit)}
loading={loading} loading={loading}

View File

@ -8,11 +8,11 @@ import ActiveUsers from './ActiveUsers';
import Arrow from 'assets/arrow-right.svg'; import Arrow from 'assets/arrow-right.svg';
import styles from './WebsiteHeader.module.css'; import styles from './WebsiteHeader.module.css';
export default function WebsiteHeader({ websiteId, token, title, showLink = false }) { export default function WebsiteHeader({ websiteId, title, showLink = false }) {
return ( return (
<PageHeader> <PageHeader>
<div className={styles.title}>{title}</div> <div className={styles.title}>{title}</div>
<ActiveUsers className={styles.active} websiteId={websiteId} token={token} /> <ActiveUsers className={styles.active} websiteId={websiteId} />
<ButtonLayout align="right"> <ButtonLayout align="right">
<RefreshButton websiteId={websiteId} /> <RefreshButton websiteId={websiteId} />
{showLink && ( {showLink && (
@ -24,7 +24,7 @@ export default function WebsiteHeader({ websiteId, token, title, showLink = fals
size="small" size="small"
iconRight iconRight
> >
<FormattedMessage id="button.view-details" defaultMessage="View details" /> <FormattedMessage id="label.view-details" defaultMessage="View details" />
</Link> </Link>
)} )}
</ButtonLayout> </ButtonLayout>

View File

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

View File

@ -0,0 +1,7 @@
.container {
display: flex;
}
.chart {
margin-bottom: 30px;
}

View File

@ -7,7 +7,7 @@ import Page from '../layout/Page';
import PageHeader from '../layout/PageHeader'; import PageHeader from '../layout/PageHeader';
import useFetch from '../../hooks/useFetch'; import useFetch from '../../hooks/useFetch';
import DropDown from '../common/DropDown'; import DropDown from '../common/DropDown';
import styles from './Test.module.css'; import styles from './TestConsole.module.css';
import WebsiteChart from '../metrics/WebsiteChart'; import WebsiteChart from '../metrics/WebsiteChart';
import EventsChart from '../metrics/EventsChart'; import EventsChart from '../metrics/EventsChart';
import Button from '../common/Button'; import Button from '../common/Button';

View File

@ -4,6 +4,7 @@ import classNames from 'classnames';
import WebsiteChart from 'components/metrics/WebsiteChart'; import WebsiteChart from 'components/metrics/WebsiteChart';
import WorldMap from 'components/common/WorldMap'; import WorldMap from 'components/common/WorldMap';
import Page from 'components/layout/Page'; import Page from 'components/layout/Page';
import GridLayout, { GridRow, GridColumn } from 'components/layout/GridLayout';
import MenuLayout from 'components/layout/MenuLayout'; import MenuLayout from 'components/layout/MenuLayout';
import Link from 'components/common/Link'; import Link from 'components/common/Link';
import Loading from 'components/common/Loading'; import Loading from 'components/common/Loading';
@ -19,6 +20,8 @@ import EventsTable from '../metrics/EventsTable';
import EventsChart from '../metrics/EventsChart'; import EventsChart from '../metrics/EventsChart';
import useFetch from 'hooks/useFetch'; import useFetch from 'hooks/useFetch';
import usePageQuery from 'hooks/usePageQuery'; import usePageQuery from 'hooks/usePageQuery';
import useShareToken from 'hooks/useShareToken';
import { DEFAULT_ANIMATION_DURATION, TOKEN_HEADER } from 'lib/constants';
const views = { const views = {
url: PagesTable, url: PagesTable,
@ -30,8 +33,11 @@ const views = {
event: EventsTable, event: EventsTable,
}; };
export default function WebsiteDetails({ websiteId, token }) { export default function WebsiteDetails({ websiteId }) {
const { data } = useFetch(`/api/website/${websiteId}`, { token }); const shareToken = useShareToken();
const { data } = useFetch(`/api/website/${websiteId}`, {
headers: { [TOKEN_HEADER]: shareToken?.token },
});
const [chartLoaded, setChartLoaded] = useState(false); const [chartLoaded, setChartLoaded] = useState(false);
const [countryData, setCountryData] = useState(); const [countryData, setCountryData] = useState();
const [eventsData, setEventsData] = useState(); const [eventsData, setEventsData] = useState();
@ -50,7 +56,7 @@ export default function WebsiteDetails({ websiteId, token }) {
icon={<Arrow />} icon={<Arrow />}
size="small" size="small"
> >
<FormattedMessage id="button.back" defaultMessage="Back" /> <FormattedMessage id="label.back" defaultMessage="Back" />
</Link> </Link>
</div> </div>
); );
@ -91,7 +97,6 @@ export default function WebsiteDetails({ websiteId, token }) {
const tableProps = { const tableProps = {
websiteId, websiteId,
token,
websiteDomain: data?.domain, websiteDomain: data?.domain,
limit: 10, limit: 10,
}; };
@ -100,7 +105,7 @@ export default function WebsiteDetails({ websiteId, token }) {
function handleDataLoad() { function handleDataLoad() {
if (!chartLoaded) { if (!chartLoaded) {
setTimeout(() => setChartLoaded(true), 300); setTimeout(() => setChartLoaded(true), DEFAULT_ANIMATION_DURATION);
} }
} }
@ -114,7 +119,6 @@ export default function WebsiteDetails({ websiteId, token }) {
<div className={classNames(styles.chart, 'col')}> <div className={classNames(styles.chart, 'col')}>
<WebsiteChart <WebsiteChart
websiteId={websiteId} websiteId={websiteId}
token={token}
title={data.name} title={data.name}
onDataLoad={handleDataLoad} onDataLoad={handleDataLoad}
showLink={false} showLink={false}
@ -124,54 +128,59 @@ export default function WebsiteDetails({ websiteId, token }) {
</div> </div>
{!chartLoaded && <Loading />} {!chartLoaded && <Loading />}
{chartLoaded && !view && ( {chartLoaded && !view && (
<> <GridLayout>
<div className={classNames(styles.row, 'row')}> <GridRow>
<div className="col-md-12 col-lg-6"> <GridColumn md={12} lg={6}>
<PagesTable {...tableProps} /> <PagesTable {...tableProps} />
</div> </GridColumn>
<div className="col-md-12 col-lg-6"> <GridColumn md={12} lg={6}>
<ReferrersTable {...tableProps} /> <ReferrersTable {...tableProps} />
</div> </GridColumn>
</div> </GridRow>
<div className={classNames(styles.row, 'row')}> <GridRow>
<div className="col-md-12 col-lg-4"> <GridColumn md={12} lg={4}>
<BrowsersTable {...tableProps} /> <BrowsersTable {...tableProps} />
</div> </GridColumn>
<div className="col-md-12 col-lg-4"> <GridColumn md={12} lg={4}>
<OSTable {...tableProps} /> <OSTable {...tableProps} />
</div> </GridColumn>
<div className="col-md-12 col-lg-4"> <GridColumn md={12} lg={4}>
<DevicesTable {...tableProps} /> <DevicesTable {...tableProps} />
</div> </GridColumn>
</div> </GridRow>
<div className={classNames(styles.row, 'row')}> <GridRow>
<div className="col-12 col-md-12 col-lg-8"> <GridColumn xs={12} md={12} lg={8}>
<WorldMap data={countryData} /> <WorldMap data={countryData} />
</div> </GridColumn>
<div className="col-12 col-md-12 col-lg-4"> <GridColumn xs={12} md={12} lg={4}>
<CountriesTable {...tableProps} onDataLoad={setCountryData} /> <CountriesTable {...tableProps} onDataLoad={setCountryData} />
</div> </GridColumn>
</div> </GridRow>
<div <GridRow className={classNames({ [styles.hidden]: !eventsData?.length > 0 })}>
className={classNames(styles.row, 'row', { [styles.hidden]: !eventsData?.length > 0 })} <GridColumn xs={12} md={12} lg={4}>
>
<div className="col-12 col-md-12 col-lg-4">
<EventsTable {...tableProps} onDataLoad={setEventsData} /> <EventsTable {...tableProps} onDataLoad={setEventsData} />
</div> </GridColumn>
<div className="col-12 col-md-12 col-lg-8 pt-5 pb-5"> <GridColumn xs={12} md={12} lg={8}>
<EventsChart websiteId={websiteId} token={token} /> <EventsChart className={styles.eventschart} websiteId={websiteId} />
</div> </GridColumn>
</div> </GridRow>
</> </GridLayout>
)} )}
{view && ( {view && chartLoaded && (
<MenuLayout <MenuLayout
className={styles.view} className={styles.view}
menuClassName={styles.menu} menuClassName={styles.menu}
contentClassName={styles.content} contentClassName={styles.content}
menu={menuOptions} menu={menuOptions}
> >
<DetailsComponent {...tableProps} limit={false} showFilters={true} /> <DetailsComponent
{...tableProps}
height={500}
limit={false}
animte={false}
showFilters
virtualize
/>
</MenuLayout> </MenuLayout>
)} )}
</Page> </Page>

View File

@ -26,37 +26,10 @@
transform: rotate(180deg); 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 { .hidden {
display: none; display: none;
} }
@media only screen and (max-width: 992px) { .eventschart {
.row { padding: 30px 0;
border: 0;
}
.row > [class*='col-'] {
border-top: 1px solid var(--gray300);
border-left: 0;
padding: 0;
}
} }

View File

@ -9,7 +9,7 @@ import Arrow from 'assets/arrow-right.svg';
import styles from './WebsiteList.module.css'; import styles from './WebsiteList.module.css';
export default function WebsiteList({ userId }) { export default function WebsiteList({ userId }) {
const { data } = useFetch('/api/websites', { user_id: userId }); const { data } = useFetch('/api/websites', { params: { user_id: userId } });
if (!data) { if (!data) {
return null; return null;

View File

@ -25,7 +25,7 @@ export default function AccountSettings() {
const [deleteAccount, setDeleteAccount] = useState(); const [deleteAccount, setDeleteAccount] = useState();
const [saved, setSaved] = useState(0); const [saved, setSaved] = useState(0);
const [message, setMessage] = useState(); 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); const Checkmark = ({ is_admin }) => (is_admin ? <Icon icon={<Check />} size="medium" /> : null);
@ -42,10 +42,10 @@ export default function AccountSettings() {
row.username !== 'admin' ? ( row.username !== 'admin' ? (
<ButtonLayout align="right"> <ButtonLayout align="right">
<Button icon={<Pen />} size="small" onClick={() => setEditAccount(row)}> <Button icon={<Pen />} size="small" onClick={() => setEditAccount(row)}>
<FormattedMessage id="button.edit" defaultMessage="Edit" /> <FormattedMessage id="label.edit" defaultMessage="Edit" />
</Button> </Button>
<Button icon={<Trash />} size="small" onClick={() => setDeleteAccount(row)}> <Button icon={<Trash />} size="small" onClick={() => setDeleteAccount(row)}>
<FormattedMessage id="button.delete" defaultMessage="Delete" /> <FormattedMessage id="label.delete" defaultMessage="Delete" />
</Button> </Button>
</ButtonLayout> </ButtonLayout>
) : null; ) : null;
@ -98,12 +98,12 @@ export default function AccountSettings() {
<FormattedMessage id="label.accounts" defaultMessage="Accounts" /> <FormattedMessage id="label.accounts" defaultMessage="Accounts" />
</div> </div>
<Button icon={<Plus />} size="small" onClick={() => setAddAccount(true)}> <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> </Button>
</PageHeader> </PageHeader>
<Table columns={columns} rows={data} /> <Table columns={columns} rows={data} />
{editAccount && ( {editAccount && (
<Modal title={<FormattedMessage id="title.edit-account" defaultMessage="Edit account" />}> <Modal title={<FormattedMessage id="label.edit-account" defaultMessage="Edit account" />}>
<AccountEditForm <AccountEditForm
values={{ ...editAccount, password: '' }} values={{ ...editAccount, password: '' }}
onSave={handleSave} onSave={handleSave}
@ -112,13 +112,13 @@ export default function AccountSettings() {
</Modal> </Modal>
)} )}
{addAccount && ( {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} /> <AccountEditForm onSave={handleSave} onClose={handleClose} />
</Modal> </Modal>
)} )}
{deleteAccount && ( {deleteAccount && (
<Modal <Modal
title={<FormattedMessage id="title.delete-account" defaultMessage="Delete account" />} title={<FormattedMessage id="label.delete-account" defaultMessage="Delete account" />}
> >
<DeleteForm <DeleteForm
values={{ type: 'account', id: deleteAccount.user_id, name: deleteAccount.username }} values={{ type: 'account', id: deleteAccount.user_id, name: deleteAccount.username }}

View File

@ -19,7 +19,7 @@ export default function DateRangeSetting() {
<> <>
<DateFilter value={value} startDate={startDate} endDate={endDate} onChange={setDateRange} /> <DateFilter value={value} startDate={startDate} endDate={endDate} onChange={setDateRange} />
<Button className={styles.button} size="small" onClick={handleReset}> <Button className={styles.button} size="small" onClick={handleReset}>
<FormattedMessage id="button.reset" defaultMessage="Reset" /> <FormattedMessage id="label.reset" defaultMessage="Reset" />
</Button> </Button>
</> </>
); );

View File

@ -34,7 +34,7 @@ export default function ProfileSettings() {
<FormattedMessage id="label.profile" defaultMessage="Profile" /> <FormattedMessage id="label.profile" defaultMessage="Profile" />
</div> </div>
<Button icon={<Dots />} size="small" onClick={() => setChangePassword(true)}> <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> </Button>
</PageHeader> </PageHeader>
<dl className={styles.list}> <dl className={styles.list}>
@ -57,7 +57,7 @@ export default function ProfileSettings() {
</dl> </dl>
{changePassword && ( {changePassword && (
<Modal <Modal
title={<FormattedMessage id="title.change-password" defaultMessage="Change password" />} title={<FormattedMessage id="label.change-password" defaultMessage="Change password" />}
> >
<ChangePasswordForm <ChangePasswordForm
values={{ user_id }} values={{ user_id }}

View File

@ -24,7 +24,7 @@ export default function TimezoneSetting() {
onChange={saveTimezone} onChange={saveTimezone}
/> />
<Button className={styles.button} size="small" onClick={handleReset}> <Button className={styles.button} size="small" onClick={handleReset}>
<FormattedMessage id="button.reset" defaultMessage="Reset" /> <FormattedMessage id="label.reset" defaultMessage="Reset" />
</Button> </Button>
</> </>
); );

View File

@ -29,7 +29,7 @@ export default function WebsiteSettings() {
const [showUrl, setShowUrl] = useState(); const [showUrl, setShowUrl] = useState();
const [saved, setSaved] = useState(0); const [saved, setSaved] = useState(0);
const [message, setMessage] = useState(); const [message, setMessage] = useState();
const { data } = useFetch(`/api/websites`, {}, { update: [saved] }); const { data } = useFetch(`/api/websites`, {}, [saved]);
const Buttons = row => ( const Buttons = row => (
<ButtonLayout align="right"> <ButtonLayout align="right">
@ -52,10 +52,10 @@ export default function WebsiteSettings() {
onClick={() => setShowCode(row)} onClick={() => setShowCode(row)}
/> />
<Button icon={<Pen />} size="small" onClick={() => setEditWebsite(row)}> <Button icon={<Pen />} size="small" onClick={() => setEditWebsite(row)}>
<FormattedMessage id="button.edit" defaultMessage="Edit" /> <FormattedMessage id="label.edit" defaultMessage="Edit" />
</Button> </Button>
<Button icon={<Trash />} size="small" onClick={() => setDeleteWebsite(row)}> <Button icon={<Trash />} size="small" onClick={() => setDeleteWebsite(row)}>
<FormattedMessage id="button.delete" defaultMessage="Delete" /> <FormattedMessage id="label.delete" defaultMessage="Delete" />
</Button> </Button>
</ButtonLayout> </ButtonLayout>
); );
@ -113,7 +113,7 @@ export default function WebsiteSettings() {
} }
> >
<Button icon={<Plus />} size="medium" onClick={() => setAddWebsite(true)}> <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> </Button>
</EmptyPlaceholder> </EmptyPlaceholder>
); );
@ -125,23 +125,23 @@ export default function WebsiteSettings() {
<FormattedMessage id="label.websites" defaultMessage="Websites" /> <FormattedMessage id="label.websites" defaultMessage="Websites" />
</div> </div>
<Button icon={<Plus />} size="small" onClick={() => setAddWebsite(true)}> <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> </Button>
</PageHeader> </PageHeader>
<Table columns={columns} rows={data} empty={empty} /> <Table columns={columns} rows={data} empty={empty} />
{editWebsite && ( {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} /> <WebsiteEditForm values={editWebsite} onSave={handleSave} onClose={handleClose} />
</Modal> </Modal>
)} )}
{addWebsite && ( {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} /> <WebsiteEditForm onSave={handleSave} onClose={handleClose} />
</Modal> </Modal>
)} )}
{deleteWebsite && ( {deleteWebsite && (
<Modal <Modal
title={<FormattedMessage id="title.delete-website" defaultMessage="Delete website" />} title={<FormattedMessage id="label.delete-website" defaultMessage="Delete website" />}
> >
<DeleteForm <DeleteForm
values={{ type: 'website', id: deleteWebsite.website_id, name: deleteWebsite.name }} values={{ type: 'website', id: deleteWebsite.website_id, name: deleteWebsite.name }}
@ -151,12 +151,12 @@ export default function WebsiteSettings() {
</Modal> </Modal>
)} )}
{showCode && ( {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} /> <TrackingCodeForm values={showCode} onClose={handleClose} />
</Modal> </Modal>
)} )}
{showUrl && ( {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} /> <ShareUrlForm values={showUrl} onClose={handleClose} />
</Modal> </Modal>
)} )}

View File

@ -4,24 +4,22 @@ import { get } from 'lib/web';
import { updateQuery } from 'redux/actions/queries'; import { updateQuery } from 'redux/actions/queries';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
export default function useFetch(url, params = {}, options = {}) { export default function useFetch(url, options = {}, update = []) {
const dispatch = useDispatch(); const dispatch = useDispatch();
const [data, setData] = useState(); const [data, setData] = useState();
const [status, setStatus] = useState(); const [status, setStatus] = useState();
const [error, setError] = useState(); const [error, setError] = useState();
const [loading, setLoadiing] = useState(false); const [loading, setLoadiing] = useState(false);
const [count, setCount] = useState(0);
const { basePath } = useRouter(); const { basePath } = useRouter();
const keys = Object.keys(params) const { params = {}, disabled, headers, delay = 0, interval, onDataLoad } = options;
.sort()
.map(key => params[key]);
const { update = [], onDataLoad = () => {} } = options;
async function loadData() { async function loadData(params) {
try { try {
setLoadiing(true); setLoadiing(true);
setError(null); setError(null);
const time = performance.now(); 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() })); dispatch(updateQuery({ url, time: performance.now() - time, completed: Date.now() }));
@ -33,7 +31,7 @@ export default function useFetch(url, params = {}, options = {}) {
} }
setStatus(status); setStatus(status);
onDataLoad(data); onDataLoad?.(data);
} catch (e) { } catch (e) {
console.error(e); console.error(e);
setError(e); setError(e);
@ -43,18 +41,24 @@ export default function useFetch(url, params = {}, options = {}) {
} }
useEffect(() => { useEffect(() => {
if (url) { if (url && !disabled) {
const { interval, delay = 0 } = options; 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 () => { return () => {
clearInterval(id); clearInterval(id);
}; };
} }
}, [url, ...keys, ...update]); }, [interval, !!disabled]);
return { data, status, error, loading }; return { data, status, error, loading };
} }

25
hooks/useShareToken.js Normal file
View File

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

View File

@ -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.accounts": "Kontoer",
"label.add-account": "Tilføj konto",
"label.add-website": "Tilføj hjemmeside",
"label.administrator": "Administrator", "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.confirm-password": "Godkendt adgangskode",
"label.copy-to-clipboard": "Kopier til udklipsholder",
"label.current-password": "Nuværende adgangskode", "label.current-password": "Nuværende adgangskode",
"label.custom-range": "Tilpasset interval", "label.custom-range": "Tilpasset interval",
"label.dashboard": "Betjeningspanel", "label.dashboard": "Betjeningspanel",
"label.date-range": "Datointerval",
"label.default-date-range": "Default date range", "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.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.enable-share-url": "Aktivér delings-URL",
"label.invalid": "Ugyldig", "label.invalid": "Ugyldig",
"label.invalid-domain": "Ugyldigt domæne", "label.invalid-domain": "Ugyldigt domæne",
"label.last-days": "Sidste {x} dage", "label.last-days": "Sidste {x} dage",
"label.last-hours": "Sidste {x} timer", "label.last-hours": "Sidste {x} timer",
"label.logged-in-as": "Loggede ind som {username}", "label.logged-in-as": "Loggede ind som {username}",
"label.login": "Log ind",
"label.logout": "Log ud", "label.logout": "Log ud",
"label.more": "Mere",
"label.name": "Navn", "label.name": "Navn",
"label.new-password": "Ny adgangskode", "label.new-password": "Ny adgangskode",
"label.password": "Adgangskode", "label.password": "Adgangskode",
"label.passwords-dont-match": "Adgangskoder matcher ikke", "label.passwords-dont-match": "Adgangskoder matcher ikke",
"label.profile": "Profil", "label.profile": "Profil",
"label.realtime": "Realtime",
"label.realtime-logs": "Realtime logs",
"label.refresh": "Opdater",
"label.required": "Påkrævet", "label.required": "Påkrævet",
"label.reset": "Reset",
"label.save": "Gem",
"label.settings": "Indstillinger", "label.settings": "Indstillinger",
"label.share-url": "Del URL",
"label.single-day": "Enkelt dag",
"label.this-month": "Denne måned", "label.this-month": "Denne måned",
"label.this-week": "Denne uge", "label.this-week": "Denne uge",
"label.this-year": "Dette år", "label.this-year": "Dette år",
"label.timezone": "Timezone", "label.timezone": "Timezone",
"label.today": "Idag", "label.today": "Idag",
"label.tracking-code": "Sporingskode",
"label.unknown": "Ukendt", "label.unknown": "Ukendt",
"label.username": "Brugernavn", "label.username": "Brugernavn",
"label.view-details": "Vis detajler",
"label.websites": "Hjemmesider", "label.websites": "Hjemmesider",
"message.active-users": "{x} nuværende {x, plural, one {bruger} other {brugere}}", "message.active-users": "{x} nuværende {x, plural, one {bruger} other {brugere}}",
"message.confirm-delete": "Er du sikker på at du vil slette {target}?", "message.confirm-delete": "Er du sikker på at du vil slette {target}?",
@ -55,6 +65,7 @@
"message.get-tracking-code": "Få sporingskode", "message.get-tracking-code": "Få sporingskode",
"message.go-to-settings": "Gå til betjeningspanel", "message.go-to-settings": "Gå til betjeningspanel",
"message.incorrect-username-password": "Ugyldigt brugernavn/adgangskode.", "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.new-version-available": "A new version of umami {version} is available!",
"message.no-data-available": "Ingen data tilgængelig.", "message.no-data-available": "Ingen data tilgængelig.",
"message.no-websites-configured": "Du har ikke konfigureret nogen websteder.", "message.no-websites-configured": "Du har ikke konfigureret nogen websteder.",
@ -84,14 +95,5 @@
"metrics.referrers": "Henvisninger", "metrics.referrers": "Henvisninger",
"metrics.unique-visitors": "Unikke besøgende", "metrics.unique-visitors": "Unikke besøgende",
"metrics.views": "Visninger", "metrics.views": "Visninger",
"metrics.visitors": "Besøgende", "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"
} }

View File

@ -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.accounts": "Konten",
"label.add-account": "Konto hinzugfügen",
"label.add-website": "Webseite hinzufügen",
"label.administrator": "Administrator", "label.administrator": "Administrator",
"label.all": "All",
"label.all-websites": "All websites",
"label.back": "Zurück",
"label.cancel": "Abbrechen",
"label.change-password": "Passwort ändern",
"label.confirm-password": "Passwort wiederholen", "label.confirm-password": "Passwort wiederholen",
"label.copy-to-clipboard": "In die Zwischenablage kopieren",
"label.current-password": "Derzeitiges Passwort", "label.current-password": "Derzeitiges Passwort",
"label.custom-range": "Benutzerdefinierter Bereich", "label.custom-range": "Benutzerdefinierter Bereich",
"label.dashboard": "Übersicht", "label.dashboard": "Übersicht",
"label.date-range": "Datumsbereich",
"label.default-date-range": "Voreingestellter 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.domain": "Domain",
"label.edit": "Bearbeiten",
"label.edit-account": "Konto bearbeiten",
"label.edit-website": "Webseite bearbeiten",
"label.enable-share-url": "Freigabe-URL aktivieren", "label.enable-share-url": "Freigabe-URL aktivieren",
"label.invalid": "Ungültig", "label.invalid": "Ungültig",
"label.invalid-domain": "Ungültige Domain", "label.invalid-domain": "Ungültige Domain",
"label.last-days": "Letzten {x} Tage", "label.last-days": "Letzten {x} Tage",
"label.last-hours": "Letzten {x} Stunden", "label.last-hours": "Letzten {x} Stunden",
"label.logged-in-as": "Angemeldet als {username}", "label.logged-in-as": "Angemeldet als {username}",
"label.login": "Anmelden",
"label.logout": "Abmelden", "label.logout": "Abmelden",
"label.more": "Mehr",
"label.name": "Name", "label.name": "Name",
"label.new-password": "Neues Passwort", "label.new-password": "Neues Passwort",
"label.password": "Passwort", "label.password": "Passwort",
"label.passwords-dont-match": "Passwörter stimmen nicht überein", "label.passwords-dont-match": "Passwörter stimmen nicht überein",
"label.profile": "Profil", "label.profile": "Profil",
"label.realtime": "Realtime",
"label.realtime-logs": "Realtime logs",
"label.refresh": "Aktualisieren",
"label.required": "Erforderlich", "label.required": "Erforderlich",
"label.reset": "Zurücksetzen",
"label.save": "Speichern",
"label.settings": "Einstellungen", "label.settings": "Einstellungen",
"label.share-url": "Freigabe-URL",
"label.single-day": "Ein Tag",
"label.this-month": "Diesen Monat", "label.this-month": "Diesen Monat",
"label.this-week": "Diese Woche", "label.this-week": "Diese Woche",
"label.this-year": "Dieses Jahr", "label.this-year": "Dieses Jahr",
"label.timezone": "Zeitzone", "label.timezone": "Zeitzone",
"label.today": "Heute", "label.today": "Heute",
"label.tracking-code": "Tracking Kennung",
"label.unknown": "Unbekannt", "label.unknown": "Unbekannt",
"label.username": "Benutzername", "label.username": "Benutzername",
"label.view-details": "Details anzeigen",
"label.websites": "Webseiten", "label.websites": "Webseiten",
"message.active-users": "{x} {x, plural, one {aktiver Besucher} other {aktive Besucher}}", "message.active-users": "{x} {x, plural, one {aktiver Besucher} other {aktive Besucher}}",
"message.confirm-delete": "Sind sie sich sicher {target} zu löschen?", "message.confirm-delete": "Sind sie sich sicher {target} zu löschen?",
@ -55,6 +65,7 @@
"message.get-tracking-code": "Erstelle Tracking Kennung", "message.get-tracking-code": "Erstelle Tracking Kennung",
"message.go-to-settings": "Zu den Einstellungen", "message.go-to-settings": "Zu den Einstellungen",
"message.incorrect-username-password": "Falsches Passwort oder Benutzername.", "message.incorrect-username-password": "Falsches Passwort oder Benutzername.",
"message.log.visitor": "Visitor from {country} using {browser} on {os} {device}",
"message.new-version-available": "Eine neue Version umami {version} ist verfügbar!", "message.new-version-available": "Eine neue Version umami {version} ist verfügbar!",
"message.no-data-available": "Keine Daten vorhanden.", "message.no-data-available": "Keine Daten vorhanden.",
"message.no-websites-configured": "Es ist keine Webseite vorhanden.", "message.no-websites-configured": "Es ist keine Webseite vorhanden.",
@ -84,14 +95,5 @@
"metrics.referrers": "Referrers", "metrics.referrers": "Referrers",
"metrics.unique-visitors": "Eindeutige Besucher", "metrics.unique-visitors": "Eindeutige Besucher",
"metrics.views": "Aufrufe", "metrics.views": "Aufrufe",
"metrics.visitors": "Besucher", "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"
} }

View File

@ -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.accounts": "Λογαριασμοί",
"label.add-account": "Προσθήκη λογαριασμού",
"label.add-website": "Προσθήκη ιστότοπου",
"label.administrator": "Διαχειριστής", "label.administrator": "Διαχειριστής",
"label.all": "All",
"label.all-websites": "All websites",
"label.back": "Πίσω",
"label.cancel": "Ακύρωση",
"label.change-password": "Αλλαγή κωδικού",
"label.confirm-password": "Επιβεβαίωση κωδικού", "label.confirm-password": "Επιβεβαίωση κωδικού",
"label.copy-to-clipboard": "Αντιγραφή στο πρόχειρο",
"label.current-password": "Τωρινός κωδικός πρόσβασης", "label.current-password": "Τωρινός κωδικός πρόσβασης",
"label.custom-range": "Προσαρμοσμένο εύρος", "label.custom-range": "Προσαρμοσμένο εύρος",
"label.dashboard": "Πίνακας", "label.dashboard": "Πίνακας",
"label.date-range": "Εύρος ημερομηνιών",
"label.default-date-range": "Προεπιλεγμένο εύρος ημερομηνιών", "label.default-date-range": "Προεπιλεγμένο εύρος ημερομηνιών",
"label.delete": "Διαγραφή",
"label.delete-account": "Διαγραφή λογαριασμού",
"label.delete-website": "Διαγραφή ιστότοπου",
"label.dismiss": "Dismiss",
"label.domain": "Τομέας", "label.domain": "Τομέας",
"label.edit": "Επεξεργασία",
"label.edit-account": "Επεξεργασία λογαριασμού",
"label.edit-website": "Επεξεργασία ιστότοπου",
"label.enable-share-url": "Ενεργοποίηση κοινής χρήσης URL", "label.enable-share-url": "Ενεργοποίηση κοινής χρήσης URL",
"label.invalid": "Μη έγκυρο", "label.invalid": "Μη έγκυρο",
"label.invalid-domain": "Μη έγκυρος τομέας", "label.invalid-domain": "Μη έγκυρος τομέας",
"label.last-days": "Τελευταίες {x} ημέρες", "label.last-days": "Τελευταίες {x} ημέρες",
"label.last-hours": "Τελευταίες {x} ώρες", "label.last-hours": "Τελευταίες {x} ώρες",
"label.logged-in-as": "Συνδεθήκατε ως {username}", "label.logged-in-as": "Συνδεθήκατε ως {username}",
"label.login": "Είσοδος",
"label.logout": "Αποσύνδεση", "label.logout": "Αποσύνδεση",
"label.more": "Περισσότερα",
"label.name": "Όνομα", "label.name": "Όνομα",
"label.new-password": "Νέος κωδικός", "label.new-password": "Νέος κωδικός",
"label.password": "Κωδικός", "label.password": "Κωδικός",
"label.passwords-dont-match": "Οι κωδικοί πρόσβασης δεν ταιριάζουν", "label.passwords-dont-match": "Οι κωδικοί πρόσβασης δεν ταιριάζουν",
"label.profile": "Προφίλ", "label.profile": "Προφίλ",
"label.realtime": "Realtime",
"label.realtime-logs": "Realtime logs",
"label.refresh": "Ανανέωση",
"label.required": "Απαιτείται", "label.required": "Απαιτείται",
"label.reset": "Επαναφορά",
"label.save": "Αποθήκευση",
"label.settings": "Ρυθμίσεις", "label.settings": "Ρυθμίσεις",
"label.share-url": "Κοινοποίηση διεύθυνσης URL",
"label.single-day": "Ημερήσια",
"label.this-month": "Αυτο το μήνα", "label.this-month": "Αυτο το μήνα",
"label.this-week": "Αυτή την εβδομάδα", "label.this-week": "Αυτή την εβδομάδα",
"label.this-year": "Αυτή την χρονιά", "label.this-year": "Αυτή την χρονιά",
"label.timezone": "Ζώνη ώρας", "label.timezone": "Ζώνη ώρας",
"label.today": "Σήμερα", "label.today": "Σήμερα",
"label.tracking-code": "Κωδικός παρακολούθησης",
"label.unknown": "Άγνωστο", "label.unknown": "Άγνωστο",
"label.username": "Όνομα χρήστη", "label.username": "Όνομα χρήστη",
"label.view-details": "Λεπτομέρειες",
"label.websites": "Ιστότοποι", "label.websites": "Ιστότοποι",
"message.active-users": "{x} ενεργοί {x, plural, one {επισκέπτης} other {επισκέπτες}}", "message.active-users": "{x} ενεργοί {x, plural, one {επισκέπτης} other {επισκέπτες}}",
"message.confirm-delete": "Είστε βέβαιοι ότι θέλετε να διαγράψετε το {target};", "message.confirm-delete": "Είστε βέβαιοι ότι θέλετε να διαγράψετε το {target};",
@ -55,6 +65,7 @@
"message.get-tracking-code": "Λήψη κώδικα παρακολούθησης", "message.get-tracking-code": "Λήψη κώδικα παρακολούθησης",
"message.go-to-settings": "Μεταβείτε στις ρυθμίσεις", "message.go-to-settings": "Μεταβείτε στις ρυθμίσεις",
"message.incorrect-username-password": "Εσφαλμένο όνομα χρήστη / κωδικός πρόσβασης.", "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.new-version-available": "A new version of umami {version} is available!",
"message.no-data-available": "Δεν υπάρχουν διαθέσιμα δεδομένα.", "message.no-data-available": "Δεν υπάρχουν διαθέσιμα δεδομένα.",
"message.no-websites-configured": "Δεν έχετε ρυθμίσει κανένα ιστότοπο.", "message.no-websites-configured": "Δεν έχετε ρυθμίσει κανένα ιστότοπο.",
@ -84,14 +95,5 @@
"metrics.referrers": "Παραπομπές", "metrics.referrers": "Παραπομπές",
"metrics.unique-visitors": "Μοναδικοί επισκέπτες", "metrics.unique-visitors": "Μοναδικοί επισκέπτες",
"metrics.views": "Προβολές", "metrics.views": "Προβολές",
"metrics.visitors": "Επισκέπτες", "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": "Κωδικός παρακολούθησης"
} }

View File

@ -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.accounts": "Accounts",
"label.add-account": "Add account",
"label.add-website": "Add website",
"label.administrator": "Administrator", "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.confirm-password": "Confirm password",
"label.copy-to-clipboard": "Copy to clipboard",
"label.current-password": "Current password", "label.current-password": "Current password",
"label.custom-range": "Custom range", "label.custom-range": "Custom range",
"label.dashboard": "Dashboard", "label.dashboard": "Dashboard",
"label.date-range": "Date range",
"label.default-date-range": "Default 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.domain": "Domain",
"label.edit": "Edit",
"label.edit-account": "Edit account",
"label.edit-website": "Edit website",
"label.enable-share-url": "Enable share URL", "label.enable-share-url": "Enable share URL",
"label.invalid": "Invalid", "label.invalid": "Invalid",
"label.invalid-domain": "Invalid domain", "label.invalid-domain": "Invalid domain",
"label.last-days": "Last {x} days", "label.last-days": "Last {x} days",
"label.last-hours": "Last {x} hours", "label.last-hours": "Last {x} hours",
"label.logged-in-as": "Logged in as {username}", "label.logged-in-as": "Logged in as {username}",
"label.login": "Login",
"label.logout": "Logout", "label.logout": "Logout",
"label.more": "More",
"label.name": "Name", "label.name": "Name",
"label.new-password": "New password", "label.new-password": "New password",
"label.password": "Password", "label.password": "Password",
"label.passwords-dont-match": "Passwords don't match", "label.passwords-dont-match": "Passwords don't match",
"label.profile": "Profile", "label.profile": "Profile",
"label.realtime": "Realtime",
"label.realtime-logs": "Realtime logs",
"label.refresh": "Refresh",
"label.required": "Required", "label.required": "Required",
"label.reset": "Reset",
"label.save": "Save",
"label.settings": "Settings", "label.settings": "Settings",
"label.share-url": "Share URL",
"label.single-day": "Single day",
"label.this-month": "This month", "label.this-month": "This month",
"label.this-week": "This week", "label.this-week": "This week",
"label.this-year": "This year", "label.this-year": "This year",
"label.timezone": "Timezone", "label.timezone": "Timezone",
"label.today": "Today", "label.today": "Today",
"label.tracking-code": "Tracking code",
"label.unknown": "Unknown", "label.unknown": "Unknown",
"label.username": "Username", "label.username": "Username",
"label.view-details": "View details",
"label.websites": "Websites", "label.websites": "Websites",
"message.active-users": "{x} current {x, plural, one {visitor} other {visitors}}", "message.active-users": "{x} current {x, plural, one {visitor} other {visitors}}",
"message.confirm-delete": "Are your sure you want to delete {target}?", "message.confirm-delete": "Are your sure you want to delete {target}?",
@ -55,6 +65,7 @@
"message.get-tracking-code": "Get tracking code", "message.get-tracking-code": "Get tracking code",
"message.go-to-settings": "Go to settings", "message.go-to-settings": "Go to settings",
"message.incorrect-username-password": "Incorrect username/password.", "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.new-version-available": "A new version of umami {version} is available!",
"message.no-data-available": "No data available.", "message.no-data-available": "No data available.",
"message.no-websites-configured": "You don't have any websites configured.", "message.no-websites-configured": "You don't have any websites configured.",
@ -84,14 +95,5 @@
"metrics.referrers": "Referrers", "metrics.referrers": "Referrers",
"metrics.unique-visitors": "Unique visitors", "metrics.unique-visitors": "Unique visitors",
"metrics.views": "Views", "metrics.views": "Views",
"metrics.visitors": "Visitors", "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"
} }

View File

@ -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.accounts": "Usuarios",
"label.add-account": "Agregar usuario",
"label.add-website": "Agregar sitio",
"label.administrator": "Administrador", "label.administrator": "Administrador",
"label.all": "All",
"label.all-websites": "All websites",
"label.back": "Atrás",
"label.cancel": "Cancelar",
"label.change-password": "Cambiar contraseña",
"label.confirm-password": "Confirmar contraseña", "label.confirm-password": "Confirmar contraseña",
"label.copy-to-clipboard": "Copiar al portapapeles",
"label.current-password": "Contraseña actual", "label.current-password": "Contraseña actual",
"label.custom-range": "Custom range", "label.custom-range": "Custom range",
"label.dashboard": "Panel de control", "label.dashboard": "Panel de control",
"label.date-range": "Date range",
"label.default-date-range": "Default date range", "label.default-date-range": "Default date range",
"label.delete": "Eliminar",
"label.delete-account": "Eliminar usuario",
"label.delete-website": "Eliminar sitio",
"label.dismiss": "Dismiss",
"label.domain": "Dominio", "label.domain": "Dominio",
"label.edit": "Editar",
"label.edit-account": "Editar usuario",
"label.edit-website": "Editar sitio",
"label.enable-share-url": "Habilitar compartir URL", "label.enable-share-url": "Habilitar compartir URL",
"label.invalid": "Inválido", "label.invalid": "Inválido",
"label.invalid-domain": "Dominio inválido", "label.invalid-domain": "Dominio inválido",
"label.last-days": "Últimos {x} días", "label.last-days": "Últimos {x} días",
"label.last-hours": "Últimas {x} horas", "label.last-hours": "Últimas {x} horas",
"label.logged-in-as": "Sesión iniciada como {username}", "label.logged-in-as": "Sesión iniciada como {username}",
"label.login": "Iniciar sesión",
"label.logout": "Cerrar sesión", "label.logout": "Cerrar sesión",
"label.more": "Más",
"label.name": "Nombre", "label.name": "Nombre",
"label.new-password": "Nueva contraseña", "label.new-password": "Nueva contraseña",
"label.password": "Contraseña", "label.password": "Contraseña",
"label.passwords-dont-match": "Las contraseñas no coinciden", "label.passwords-dont-match": "Las contraseñas no coinciden",
"label.profile": "Perfil", "label.profile": "Perfil",
"label.realtime": "Realtime",
"label.realtime-logs": "Realtime logs",
"label.refresh": "Refresh",
"label.required": "Requerido", "label.required": "Requerido",
"label.reset": "Reset",
"label.save": "Guardar",
"label.settings": "Configuraciones", "label.settings": "Configuraciones",
"label.share-url": "Compartir URL",
"label.single-day": "Single day",
"label.this-month": "Este mes", "label.this-month": "Este mes",
"label.this-week": "Esta semana", "label.this-week": "Esta semana",
"label.this-year": "Este año", "label.this-year": "Este año",
"label.timezone": "Timezone", "label.timezone": "Timezone",
"label.today": "Hoy", "label.today": "Hoy",
"label.tracking-code": "Código de rastreo",
"label.unknown": "Unknown", "label.unknown": "Unknown",
"label.username": "Nombre de usuario", "label.username": "Nombre de usuario",
"label.view-details": "Ver detalles",
"label.websites": "Sitios", "label.websites": "Sitios",
"message.active-users": "{x} {x, plural, one {activo} other {activos}}", "message.active-users": "{x} {x, plural, one {activo} other {activos}}",
"message.confirm-delete": "¿Estás seguro(a) de querer eliminar {target}?", "message.confirm-delete": "¿Estás seguro(a) de querer eliminar {target}?",
@ -55,6 +65,7 @@
"message.get-tracking-code": "Obtener código de rastreo", "message.get-tracking-code": "Obtener código de rastreo",
"message.go-to-settings": "Ir a la configuración", "message.go-to-settings": "Ir a la configuración",
"message.incorrect-username-password": "Nombre de usuario o contraseña incorrectos.", "message.incorrect-username-password": "Nombre de usuario o contraseña incorrectos.",
"message.log.visitor": "Visitor from {country} using {browser} on {os} {device}",
"message.new-version-available": "A new version of umami {version} is available!", "message.new-version-available": "A new version of umami {version} is available!",
"message.no-data-available": "Sin información disponible.", "message.no-data-available": "Sin información disponible.",
"message.no-websites-configured": "No tienes ningún sitio configurado.", "message.no-websites-configured": "No tienes ningún sitio configurado.",
@ -84,14 +95,5 @@
"metrics.referrers": "Referentes", "metrics.referrers": "Referentes",
"metrics.unique-visitors": "Visitantes únicos", "metrics.unique-visitors": "Visitantes únicos",
"metrics.views": "Vistas", "metrics.views": "Vistas",
"metrics.visitors": "Visitantes", "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"
} }

99
lang/fi-FI.json Normal file
View File

@ -0,0 +1,99 @@
{
"label.accounts": "Tilit",
"label.add-account": "Lisää tili",
"label.add-website": "Lisää verkkosivu",
"label.administrator": "Järjestelmänvalvoja",
"label.all": "All",
"label.all-websites": "All websites",
"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": "Dashboard",
"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": "Realtime",
"label.realtime-logs": "Realtime logs",
"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": "Visitor from {country} using {browser} on {os} {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"
}

View File

@ -1,50 +1,60 @@
{ {
"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.accounts": "Brúkarar",
"label.add-account": "Ger brúkara",
"label.add-website": "Legg heimasíðu avtrat",
"label.administrator": "Administrator", "label.administrator": "Administrator",
"label.all": "All",
"label.all-websites": "All websites",
"label.back": "Aftur",
"label.cancel": "Strika",
"label.change-password": "Skift loyniorð",
"label.confirm-password": "Vátta loyniorð", "label.confirm-password": "Vátta loyniorð",
"label.copy-to-clipboard": "Kopier til clipboard",
"label.current-password": "Núverandi loyniorð", "label.current-password": "Núverandi loyniorð",
"label.custom-range": "Tillaga spenni", "label.custom-range": "Tillaga spenni",
"label.dashboard": "Yvirlitsskíggi", "label.dashboard": "Yvirlitsskíggi",
"label.date-range": "Vel dato",
"label.default-date-range": "Standard dato", "label.default-date-range": "Standard dato",
"label.delete": "Sletta",
"label.delete-account": "Sletta brúkara",
"label.delete-website": "Sletta heimasíðu",
"label.dismiss": "Dismiss",
"label.domain": "Økisnavn", "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.enable-share-url": "Virkja deili leinki",
"label.invalid": "Ógilda", "label.invalid": "Ógilda",
"label.invalid-domain": "Ógilt økisnavn", "label.invalid-domain": "Ógilt økisnavn",
"label.last-days": "Seinastu {x} dagarnar", "label.last-days": "Seinastu {x} dagarnar",
"label.last-hours": "Seinastu {x} tímanar", "label.last-hours": "Seinastu {x} tímanar",
"label.logged-in-as": "Ritaður inn sum {username}", "label.logged-in-as": "Ritaður inn sum {username}",
"label.login": "Rita inn",
"label.logout": "Rita út", "label.logout": "Rita út",
"label.more": "Meira",
"label.name": "Navn", "label.name": "Navn",
"label.new-password": "Nýtt loyniorð", "label.new-password": "Nýtt loyniorð",
"label.password": "Loyniorð", "label.password": "Loyniorð",
"label.passwords-dont-match": "Loyniorðini eru ikki eins", "label.passwords-dont-match": "Loyniorðini eru ikki eins",
"label.profile": "Brúkari", "label.profile": "Brúkari",
"label.realtime": "Realtime",
"label.realtime-logs": "Realtime logs",
"label.refresh": "Endurskapa",
"label.required": "Krav", "label.required": "Krav",
"label.reset": "Nulstilla",
"label.save": "Goym",
"label.settings": "Stillingar", "label.settings": "Stillingar",
"label.share-url": "Deil leinku",
"label.single-day": "Einkultur dagur",
"label.this-month": "Hendan mánan", "label.this-month": "Hendan mánan",
"label.this-week": "Hesa vikuna", "label.this-week": "Hesa vikuna",
"label.this-year": "Hetta árið", "label.this-year": "Hetta árið",
"label.timezone": "Tíðarsona", "label.timezone": "Tíðarsona",
"label.today": "Í dag", "label.today": "Í dag",
"label.tracking-code": "Spori kota",
"label.unknown": "Ókent", "label.unknown": "Ókent",
"label.username": "Brúkaranavn", "label.username": "Brúkaranavn",
"label.view-details": "Vís upplýsingar",
"label.websites": "Heimasíður", "label.websites": "Heimasíður",
"message.active-users": "{x} í løtuni {x, plural, one {vitjandi} other { vitjandi }}", "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.confirm-delete": "Ert tú sikkur at tú ynskir at sletta {target}?",
@ -55,6 +65,7 @@
"message.get-tracking-code": "Fá sporings kotu", "message.get-tracking-code": "Fá sporings kotu",
"message.go-to-settings": "Far til stillingar", "message.go-to-settings": "Far til stillingar",
"message.incorrect-username-password": "Skeivt brúkaranavn/loyniorð.", "message.incorrect-username-password": "Skeivt brúkaranavn/loyniorð.",
"message.log.visitor": "Visitor from {country} using {browser} on {os} {device}",
"message.new-version-available": "A new version of umami {version} is available!", "message.new-version-available": "A new version of umami {version} is available!",
"message.no-data-available": "Einki data tøk.", "message.no-data-available": "Einki data tøk.",
"message.no-websites-configured": "Tú hevur ongar heimasíður stillaða til.", "message.no-websites-configured": "Tú hevur ongar heimasíður stillaða til.",
@ -84,14 +95,5 @@
"metrics.referrers": "Framsendingar", "metrics.referrers": "Framsendingar",
"metrics.unique-visitors": "Einsýna vitjanir", "metrics.unique-visitors": "Einsýna vitjanir",
"metrics.views": "Vitjanir", "metrics.views": "Vitjanir",
"metrics.visitors": "Vitjandi", "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"
} }

View File

@ -1,50 +1,60 @@
{ {
"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.accounts": "Comptes",
"label.add-account": "Ajouter un compte",
"label.add-website": "Ajouter un site",
"label.administrator": "Administrateur", "label.administrator": "Administrateur",
"label.all": "All",
"label.all-websites": "All websites",
"label.back": "Retour",
"label.cancel": "Annuler",
"label.change-password": "Changer le mot de passe",
"label.confirm-password": "Confirmation du 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.current-password": "Mot de passe actuel",
"label.custom-range": "Plage personnalisée", "label.custom-range": "Plage personnalisée",
"label.dashboard": "Tableau de bord", "label.dashboard": "Tableau de bord",
"label.date-range": "Date range",
"label.default-date-range": "Default date range", "label.default-date-range": "Default date range",
"label.delete": "Supprimer",
"label.delete-account": "Supprimer le compte",
"label.delete-website": "Suprimer le site",
"label.dismiss": "Dismiss",
"label.domain": "Domaine", "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.enable-share-url": "Activer le partage d'URL",
"label.invalid": "Invalide", "label.invalid": "Invalide",
"label.invalid-domain": "Domaine invalide", "label.invalid-domain": "Domaine invalide",
"label.last-days": "{x} derniers jours", "label.last-days": "{x} derniers jours",
"label.last-hours": "{x} dernières heures", "label.last-hours": "{x} dernières heures",
"label.logged-in-as": "Connecté en tant que {username}", "label.logged-in-as": "Connecté en tant que {username}",
"label.login": "Connexion",
"label.logout": "Déconnexion", "label.logout": "Déconnexion",
"label.more": "Plus",
"label.name": "Nom", "label.name": "Nom",
"label.new-password": "Nouveau mot de passe", "label.new-password": "Nouveau mot de passe",
"label.password": "Mot de passe", "label.password": "Mot de passe",
"label.passwords-dont-match": "Les mots de passe ne correspondent pas", "label.passwords-dont-match": "Les mots de passe ne correspondent pas",
"label.profile": "Profile", "label.profile": "Profile",
"label.realtime": "Realtime",
"label.realtime-logs": "Realtime logs",
"label.refresh": "Refresh",
"label.required": "Requis", "label.required": "Requis",
"label.reset": "Reset",
"label.save": "Sauvegarder",
"label.settings": "Paramètres", "label.settings": "Paramètres",
"label.share-url": "Partager l'URL",
"label.single-day": "Single day",
"label.this-month": "Ce mois ci", "label.this-month": "Ce mois ci",
"label.this-week": "Cette semaine", "label.this-week": "Cette semaine",
"label.this-year": "Cette année", "label.this-year": "Cette année",
"label.timezone": "Timezone", "label.timezone": "Timezone",
"label.today": "Aujourd'hui", "label.today": "Aujourd'hui",
"label.tracking-code": "Code de suivi",
"label.unknown": "Unknown", "label.unknown": "Unknown",
"label.username": "Nom d'utilisateur", "label.username": "Nom d'utilisateur",
"label.view-details": "Voir les details",
"label.websites": "Sites", "label.websites": "Sites",
"message.active-users": "{x} {x, plural, one {visiteur} other {visiteurs}} actuellement", "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 sur de vouloir supprimer {target}?",
@ -55,6 +65,7 @@
"message.get-tracking-code": "Obtenez le code de suivi", "message.get-tracking-code": "Obtenez le code de suivi",
"message.go-to-settings": "Aller aux paramètres", "message.go-to-settings": "Aller aux paramètres",
"message.incorrect-username-password": "nom d'utilisateurs/mot de passe incorrect.", "message.incorrect-username-password": "nom d'utilisateurs/mot de passe incorrect.",
"message.log.visitor": "Visitor from {country} using {browser} on {os} {device}",
"message.new-version-available": "A new version of umami {version} is available!", "message.new-version-available": "A new version of umami {version} is available!",
"message.no-data-available": "Pas de données disponibles.", "message.no-data-available": "Pas de données disponibles.",
"message.no-websites-configured": "Vous n'avez configuré aucun site Web.", "message.no-websites-configured": "Vous n'avez configuré aucun site Web.",
@ -84,14 +95,5 @@
"metrics.referrers": "URL Référentes", "metrics.referrers": "URL Référentes",
"metrics.unique-visitors": "Visiteurs uniques", "metrics.unique-visitors": "Visiteurs uniques",
"metrics.views": "Vues", "metrics.views": "Vues",
"metrics.visitors": "Visiteurs", "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"
} }

View File

@ -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.accounts": "Akun",
"label.add-account": "Tambah akun",
"label.add-website": "Tambah situs web",
"label.administrator": "Pengelola", "label.administrator": "Pengelola",
"label.all": "All",
"label.all-websites": "All websites",
"label.back": "Kembali",
"label.cancel": "Batal",
"label.change-password": "Ganti kata sandi",
"label.confirm-password": "Konfirmasi kata sandi", "label.confirm-password": "Konfirmasi kata sandi",
"label.copy-to-clipboard": "Salin ke papan klip",
"label.current-password": "Kata sandi sekarang", "label.current-password": "Kata sandi sekarang",
"label.custom-range": "Rentang khusus", "label.custom-range": "Rentang khusus",
"label.dashboard": "Dasbor", "label.dashboard": "Dasbor",
"label.date-range": "Rentang tanggal",
"label.default-date-range": "Rentang tanggal default", "label.default-date-range": "Rentang tanggal default",
"label.delete": "Hapus",
"label.delete-account": "Hapus akun",
"label.delete-website": "Hapus situs web",
"label.dismiss": "Tutup",
"label.domain": "Domain", "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.enable-share-url": "Aktifkan URL berbagi",
"label.invalid": "Tidak valid", "label.invalid": "Tidak valid",
"label.invalid-domain": "Domain tidak valid", "label.invalid-domain": "Domain tidak valid",
"label.last-days": "{x} hari terakhir", "label.last-days": "{x} hari terakhir",
"label.last-hours": "{x} jam terakhir", "label.last-hours": "{x} jam terakhir",
"label.logged-in-as": "Masuk sebagai {username}", "label.logged-in-as": "Masuk sebagai {username}",
"label.login": "Masuk",
"label.logout": "Keluar", "label.logout": "Keluar",
"label.more": "Lebih banyak",
"label.name": "Nama", "label.name": "Nama",
"label.new-password": "Kata sandi baru", "label.new-password": "Kata sandi baru",
"label.password": "Kata sandi", "label.password": "Kata sandi",
"label.passwords-dont-match": "Kata sandi tidak cocok", "label.passwords-dont-match": "Kata sandi tidak cocok",
"label.profile": "Profil", "label.profile": "Profil",
"label.realtime": "Realtime",
"label.realtime-logs": "Realtime logs",
"label.refresh": "Segarkan",
"label.required": "Wajib", "label.required": "Wajib",
"label.reset": "Atur ulang",
"label.save": "Simpan",
"label.settings": "Pengaturan", "label.settings": "Pengaturan",
"label.share-url": "Bagikan URL",
"label.single-day": "Sehari",
"label.this-month": "Bulan ini", "label.this-month": "Bulan ini",
"label.this-week": "Minggu ini", "label.this-week": "Minggu ini",
"label.this-year": "Tahun ini", "label.this-year": "Tahun ini",
"label.timezone": "Zona waktu", "label.timezone": "Zona waktu",
"label.today": "Hari ini", "label.today": "Hari ini",
"label.tracking-code": "Kode lacak",
"label.unknown": "Tidak diketahui", "label.unknown": "Tidak diketahui",
"label.username": "Nama pengguna", "label.username": "Nama pengguna",
"label.view-details": "Lihat Detil",
"label.websites": "Situs web", "label.websites": "Situs web",
"message.active-users": "{x} pengunjung saat ini", "message.active-users": "{x} pengunjung saat ini",
"message.confirm-delete": "Apakah kamu yakin ingin menghapus {target}?", "message.confirm-delete": "Apakah kamu yakin ingin menghapus {target}?",
@ -55,6 +65,7 @@
"message.get-tracking-code": "Dapatkan kode pelacakan", "message.get-tracking-code": "Dapatkan kode pelacakan",
"message.go-to-settings": "Pergi ke pengaturan", "message.go-to-settings": "Pergi ke pengaturan",
"message.incorrect-username-password": "Nama pengguna/kata sandi salah.", "message.incorrect-username-password": "Nama pengguna/kata sandi salah.",
"message.log.visitor": "Visitor from {country} using {browser} on {os} {device}",
"message.new-version-available": "Versi terbaru umami {version} telah tersedia!", "message.new-version-available": "Versi terbaru umami {version} telah tersedia!",
"message.no-data-available": "Tidak ada data.", "message.no-data-available": "Tidak ada data.",
"message.no-websites-configured": "Anda tidak memiliki situs web yang dikonfigurasi.", "message.no-websites-configured": "Anda tidak memiliki situs web yang dikonfigurasi.",
@ -84,14 +95,5 @@
"metrics.referrers": "Perujuk", "metrics.referrers": "Perujuk",
"metrics.unique-visitors": "Pengunjung unik", "metrics.unique-visitors": "Pengunjung unik",
"metrics.views": "Tampilan", "metrics.views": "Tampilan",
"metrics.visitors": "Pengunjung", "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"
} }

View File

@ -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.accounts": "アカウント",
"label.add-account": "アカウントの追加",
"label.add-website": "Webサイトの追加",
"label.administrator": "管理者", "label.administrator": "管理者",
"label.all": "All",
"label.all-websites": "All websites",
"label.back": "戻る",
"label.cancel": "キャンセル",
"label.change-password": "パスワード変更",
"label.confirm-password": "パスワード(確認)", "label.confirm-password": "パスワード(確認)",
"label.copy-to-clipboard": "クリップボードにコピー",
"label.current-password": "現在のパスワード", "label.current-password": "現在のパスワード",
"label.custom-range": "期間を指定する", "label.custom-range": "期間を指定する",
"label.dashboard": "ダッシュボード", "label.dashboard": "ダッシュボード",
"label.date-range": "日付範囲",
"label.default-date-range": "最初に表示する期間", "label.default-date-range": "最初に表示する期間",
"label.delete": "削除",
"label.delete-account": "アカウントの削除",
"label.delete-website": "Webサイトの削除",
"label.dismiss": "無視する",
"label.domain": "ドメイン", "label.domain": "ドメイン",
"label.edit": "編集",
"label.edit-account": "アカウントの編集",
"label.edit-website": "Webサイトの編集",
"label.enable-share-url": "共有リンクを有効にする", "label.enable-share-url": "共有リンクを有効にする",
"label.invalid": "無効", "label.invalid": "無効",
"label.invalid-domain": "無効なドメイン", "label.invalid-domain": "無効なドメイン",
"label.last-days": "過去{x}日間", "label.last-days": "過去{x}日間",
"label.last-hours": "過去{x}時間", "label.last-hours": "過去{x}時間",
"label.logged-in-as": "{username}でログイン中", "label.logged-in-as": "{username}でログイン中",
"label.login": "ログイン",
"label.logout": "ログアウト", "label.logout": "ログアウト",
"label.more": "さらに表示",
"label.name": "名前", "label.name": "名前",
"label.new-password": "新しいパスワード", "label.new-password": "新しいパスワード",
"label.password": "パスワード", "label.password": "パスワード",
"label.passwords-dont-match": "パスワードが一致しません", "label.passwords-dont-match": "パスワードが一致しません",
"label.profile": "プロファイル", "label.profile": "プロファイル",
"label.realtime": "Realtime",
"label.realtime-logs": "Realtime logs",
"label.refresh": "更新",
"label.required": "必須", "label.required": "必須",
"label.reset": "リセット",
"label.save": "保存",
"label.settings": "設定", "label.settings": "設定",
"label.share-url": "共有リンク",
"label.single-day": "一日のみ",
"label.this-month": "今月", "label.this-month": "今月",
"label.this-week": "今週", "label.this-week": "今週",
"label.this-year": "今年", "label.this-year": "今年",
"label.timezone": "タイムゾーン", "label.timezone": "タイムゾーン",
"label.today": "今日", "label.today": "今日",
"label.tracking-code": "トラッキングコード",
"label.unknown": "不明", "label.unknown": "不明",
"label.username": "ユーザー名", "label.username": "ユーザー名",
"label.view-details": "詳細を見る",
"label.websites": "Webサイト", "label.websites": "Webサイト",
"message.active-users": "{x}人が閲覧中です。", "message.active-users": "{x}人が閲覧中です。",
"message.confirm-delete": "{target}を削除してもよろしいですか?", "message.confirm-delete": "{target}を削除してもよろしいですか?",
@ -55,6 +65,7 @@
"message.get-tracking-code": "トラッキングコードを取得", "message.get-tracking-code": "トラッキングコードを取得",
"message.go-to-settings": "設定する", "message.go-to-settings": "設定する",
"message.incorrect-username-password": "ユーザー名/パスワードが正しくありません。", "message.incorrect-username-password": "ユーザー名/パスワードが正しくありません。",
"message.log.visitor": "Visitor from {country} using {browser} on {os} {device}",
"message.new-version-available": "新しいバージョン({version})が利用可能です!", "message.new-version-available": "新しいバージョン({version})が利用可能です!",
"message.no-data-available": "データがありません。", "message.no-data-available": "データがありません。",
"message.no-websites-configured": "Webサイトが設定されていません。", "message.no-websites-configured": "Webサイトが設定されていません。",
@ -84,14 +95,5 @@
"metrics.referrers": "リファラー", "metrics.referrers": "リファラー",
"metrics.unique-visitors": "ユニーク訪問者数", "metrics.unique-visitors": "ユニーク訪問者数",
"metrics.views": "閲覧数", "metrics.views": "閲覧数",
"metrics.visitors": "訪問者数", "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": "トラッキングコード"
} }

View File

@ -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.accounts": "Хэрэглэгчид",
"label.add-account": "Хэрэглэгч нэмэх",
"label.add-website": "Веб нэмэх",
"label.administrator": "Админ", "label.administrator": "Админ",
"label.all": "All",
"label.all-websites": "All websites",
"label.back": "Буцах",
"label.cancel": "Цуцлах",
"label.change-password": "Нууц үг солих",
"label.confirm-password": "Шинэ нууц үгээ давтах", "label.confirm-password": "Шинэ нууц үгээ давтах",
"label.copy-to-clipboard": "Хуулах",
"label.current-password": "Ашиглаж буй нууц үг", "label.current-password": "Ашиглаж буй нууц үг",
"label.custom-range": "Дурын хугацаа", "label.custom-range": "Дурын хугацаа",
"label.dashboard": "Хянах самбар", "label.dashboard": "Хянах самбар",
"label.date-range": "Хугацааны мужид",
"label.default-date-range": "Өгөгдмөл хугацааны муж", "label.default-date-range": "Өгөгдмөл хугацааны муж",
"label.delete": "Устгах",
"label.delete-account": "Хэрэглэгч устгах",
"label.delete-website": "Веб устгах",
"label.dismiss": "Үл хэргэсэх",
"label.domain": "Домэйн", "label.domain": "Домэйн",
"label.edit": "Засах",
"label.edit-account": "Хэрэглэгч засах",
"label.edit-website": "Веб засах",
"label.enable-share-url": "Хуваалцах холбоос идэвхжүүлэх", "label.enable-share-url": "Хуваалцах холбоос идэвхжүүлэх",
"label.invalid": "Буруу", "label.invalid": "Буруу",
"label.invalid-domain": "Буруу домэйн", "label.invalid-domain": "Буруу домэйн",
"label.last-days": "Сүүлийн {x} хоног", "label.last-days": "Сүүлийн {x} хоног",
"label.last-hours": "Сүүлийн {x} цаг", "label.last-hours": "Сүүлийн {x} цаг",
"label.logged-in-as": "{username}-р нэвтэрсэн", "label.logged-in-as": "{username}-р нэвтэрсэн",
"label.login": "Нэвтрэх",
"label.logout": "Гарах", "label.logout": "Гарах",
"label.more": "Цааш",
"label.name": "Нэр", "label.name": "Нэр",
"label.new-password": "Шинэ нууц үг", "label.new-password": "Шинэ нууц үг",
"label.password": "Нууц үг", "label.password": "Нууц үг",
"label.passwords-dont-match": "Нууц үг тохирохгүй байна", "label.passwords-dont-match": "Нууц үг тохирохгүй байна",
"label.profile": "Бүртгэл", "label.profile": "Бүртгэл",
"label.realtime": "Realtime",
"label.realtime-logs": "Realtime logs",
"label.refresh": "Сэргээх",
"label.required": "Шаардлагатай", "label.required": "Шаардлагатай",
"label.reset": "Хуучин хэвд нь оруулах",
"label.save": "Хадгалах",
"label.settings": "Тохиргоо", "label.settings": "Тохиргоо",
"label.share-url": "Хуваалцах холбоос",
"label.single-day": "Нэг өдөр",
"label.this-month": "Энэ сар", "label.this-month": "Энэ сар",
"label.this-week": "Энэ долоо хоног", "label.this-week": "Энэ долоо хоног",
"label.this-year": "Энэ жил", "label.this-year": "Энэ жил",
"label.timezone": "Цагийн бүс", "label.timezone": "Цагийн бүс",
"label.today": "Өнөөдөр", "label.today": "Өнөөдөр",
"label.tracking-code": "Мөрдөх код",
"label.unknown": "Тодорхойгүй", "label.unknown": "Тодорхойгүй",
"label.username": "Хэрэглэгчийн нэр", "label.username": "Хэрэглэгчийн нэр",
"label.view-details": "Дэлгэрүүлж харах",
"label.websites": "Вебүүд", "label.websites": "Вебүүд",
"message.active-users": "одоо {x} {x, plural, one {зочин} other {зочин}} байна", "message.active-users": "одоо {x} {x, plural, one {зочин} other {зочин}} байна",
"message.confirm-delete": "Та {target}-г устгахдаа итгэлтэй байна уу?", "message.confirm-delete": "Та {target}-г устгахдаа итгэлтэй байна уу?",
@ -55,6 +65,7 @@
"message.get-tracking-code": "Мөрдөх код авах", "message.get-tracking-code": "Мөрдөх код авах",
"message.go-to-settings": "Тохиргоо руу очих", "message.go-to-settings": "Тохиргоо руу очих",
"message.incorrect-username-password": "Буруу хэрэглэгчийн нэр/нууц үг.", "message.incorrect-username-password": "Буруу хэрэглэгчийн нэр/нууц үг.",
"message.log.visitor": "Visitor from {country} using {browser} on {os} {device}",
"message.new-version-available": "Umami-гийн шинэ хувилбар {version} гарсан байна!", "message.new-version-available": "Umami-гийн шинэ хувилбар {version} гарсан байна!",
"message.no-data-available": "Өгөгдөл алга.", "message.no-data-available": "Өгөгдөл алга.",
"message.no-websites-configured": "Та ямар нэгэн веб тохируулаагүй байна.", "message.no-websites-configured": "Та ямар нэгэн веб тохируулаагүй байна.",
@ -84,14 +95,5 @@
"metrics.referrers": "Чиглүүлэгч", "metrics.referrers": "Чиглүүлэгч",
"metrics.unique-visitors": "Зочид", "metrics.unique-visitors": "Зочид",
"metrics.views": "Үзсэн", "metrics.views": "Үзсэн",
"metrics.visitors": "Зочид", "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": "Мөрдөх код"
} }

View File

@ -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.accounts": "Kontoer",
"label.add-account": "Legg til konto",
"label.add-website": "Legg til nettsted",
"label.administrator": "Administrator", "label.administrator": "Administrator",
"label.all": "All",
"label.all-websites": "All websites",
"label.back": "Tilbake",
"label.cancel": "Avvis",
"label.change-password": "Bytt passord",
"label.confirm-password": "Godkjenn passord", "label.confirm-password": "Godkjenn passord",
"label.copy-to-clipboard": "Kopier til utklippstavle",
"label.current-password": "Nåværende passord", "label.current-password": "Nåværende passord",
"label.custom-range": "Egendefinert utvalg", "label.custom-range": "Egendefinert utvalg",
"label.dashboard": "Dashboard", "label.dashboard": "Dashboard",
"label.date-range": "Datointervall",
"label.default-date-range": "Standard datoperiode", "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.domain": "Domene",
"label.edit": "Rediger",
"label.edit-account": "Rediger konto",
"label.edit-website": "Rediger nettsted",
"label.enable-share-url": "Aktiver delings-URL", "label.enable-share-url": "Aktiver delings-URL",
"label.invalid": "Ugyldig", "label.invalid": "Ugyldig",
"label.invalid-domain": "Ugyldig domene", "label.invalid-domain": "Ugyldig domene",
"label.last-days": "Siste {x} dager", "label.last-days": "Siste {x} dager",
"label.last-hours": "Siste {x} timer", "label.last-hours": "Siste {x} timer",
"label.logged-in-as": "Logget på som {brukernavn}", "label.logged-in-as": "Logget på som {brukernavn}",
"label.login": "Logg inn",
"label.logout": "Logg ut", "label.logout": "Logg ut",
"label.more": "Mer",
"label.name": "Navn", "label.name": "Navn",
"label.new-password": "Nytt passord", "label.new-password": "Nytt passord",
"label.password": "Passord", "label.password": "Passord",
"label.passwords-dont-match": "Passordene er ikke like", "label.passwords-dont-match": "Passordene er ikke like",
"label.profile": "Profil", "label.profile": "Profil",
"label.realtime": "Realtime",
"label.realtime-logs": "Realtime logs",
"label.refresh": "Oppdater",
"label.required": "Påkrevd", "label.required": "Påkrevd",
"label.reset": "Nullstill",
"label.save": "Lagre",
"label.settings": "Innstillinger", "label.settings": "Innstillinger",
"label.share-url": "Del URL",
"label.single-day": "Enkelt dag",
"label.this-month": "Denne måneden", "label.this-month": "Denne måneden",
"label.this-week": "Denne uka", "label.this-week": "Denne uka",
"label.this-year": "I år", "label.this-year": "I år",
"label.timezone": "Tidssone", "label.timezone": "Tidssone",
"label.today": "I dag", "label.today": "I dag",
"label.tracking-code": "Sporingskode",
"label.unknown": "Ukjent", "label.unknown": "Ukjent",
"label.username": "Brukernavn", "label.username": "Brukernavn",
"label.view-details": "Vis detaljer",
"label.websites": "Nettsteder", "label.websites": "Nettsteder",
"message.active-users": "{x} {x, plural, one {besøkende} other {besøkende}} nå", "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}?", "message.confirm-delete": "Er du sikker på at du vil slette {target}?",
@ -55,6 +65,7 @@
"message.get-tracking-code": "Få sporingskode", "message.get-tracking-code": "Få sporingskode",
"message.go-to-settings": "Gå til innstillinger", "message.go-to-settings": "Gå til innstillinger",
"message.incorrect-username-password": "Ugyldig brukernavn/passord.", "message.incorrect-username-password": "Ugyldig brukernavn/passord.",
"message.log.visitor": "Visitor from {country} using {browser} on {os} {device}",
"message.new-version-available": "En ny versjon av umami {version} er tilgjengelig!", "message.new-version-available": "En ny versjon av umami {version} er tilgjengelig!",
"message.no-data-available": "Ingen data tilgjengelig.", "message.no-data-available": "Ingen data tilgjengelig.",
"message.no-websites-configured": "Du har ikke satt opp noen nettsteder.", "message.no-websites-configured": "Du har ikke satt opp noen nettsteder.",
@ -84,14 +95,5 @@
"metrics.referrers": "Referanser", "metrics.referrers": "Referanser",
"metrics.unique-visitors": "Unike besøkende", "metrics.unique-visitors": "Unike besøkende",
"metrics.views": "Visninger", "metrics.views": "Visninger",
"metrics.visitors": "Besøkende", "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"
} }

View File

@ -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.accounts": "Gebruikers",
"label.add-account": "Account toevoegen",
"label.add-website": "Website toevoegen",
"label.administrator": "Administrator", "label.administrator": "Administrator",
"label.all": "All",
"label.all-websites": "All websites",
"label.back": "Terug",
"label.cancel": "Annuleren",
"label.change-password": "Wachtwoord wijzigen",
"label.confirm-password": "Wachtwoord bevestigen", "label.confirm-password": "Wachtwoord bevestigen",
"label.copy-to-clipboard": "Kopiëer naar klembord",
"label.current-password": "Huidig wachtwoord", "label.current-password": "Huidig wachtwoord",
"label.custom-range": "Aangepast bereik", "label.custom-range": "Aangepast bereik",
"label.dashboard": "Overzicht", "label.dashboard": "Overzicht",
"label.date-range": "Datumbereik",
"label.default-date-range": "Standaard bereik", "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.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.enable-share-url": "Sta delen via openbare URL toe",
"label.invalid": "Ongeldig", "label.invalid": "Ongeldig",
"label.invalid-domain": "Ongeldig domein", "label.invalid-domain": "Ongeldig domein",
"label.last-days": "Laatste {x} dagen", "label.last-days": "Laatste {x} dagen",
"label.last-hours": "Laatste {x} uur", "label.last-hours": "Laatste {x} uur",
"label.logged-in-as": "Ingelogd als {username}", "label.logged-in-as": "Ingelogd als {username}",
"label.login": "Inloggen",
"label.logout": "Uitloggen", "label.logout": "Uitloggen",
"label.more": "Toon meer",
"label.name": "Naam", "label.name": "Naam",
"label.new-password": "Nieuw wachtwoord", "label.new-password": "Nieuw wachtwoord",
"label.password": "Wachtwoord", "label.password": "Wachtwoord",
"label.passwords-dont-match": "Wachtwoorden komen niet overeen", "label.passwords-dont-match": "Wachtwoorden komen niet overeen",
"label.profile": "Profiel", "label.profile": "Profiel",
"label.realtime": "Realtime",
"label.realtime-logs": "Realtime logs",
"label.refresh": "Vernieuwen",
"label.required": "Verplicht", "label.required": "Verplicht",
"label.reset": "Resetten",
"label.save": "Opslaan",
"label.settings": "Instellingen", "label.settings": "Instellingen",
"label.share-url": "URL delen",
"label.single-day": "Enkele dag",
"label.this-month": "Deze maand", "label.this-month": "Deze maand",
"label.this-week": "Deze week", "label.this-week": "Deze week",
"label.this-year": "Dit jaar", "label.this-year": "Dit jaar",
"label.timezone": "Tijdzone", "label.timezone": "Tijdzone",
"label.today": "Vandaag", "label.today": "Vandaag",
"label.tracking-code": "Tracking code",
"label.unknown": "Onbekend", "label.unknown": "Onbekend",
"label.username": "Gebruikersnaam", "label.username": "Gebruikersnaam",
"label.view-details": "Meer details",
"label.websites": "Websites", "label.websites": "Websites",
"message.active-users": "{x} actieve {x, plural, one {bezoeker} other {bezoekers}}", "message.active-users": "{x} actieve {x, plural, one {bezoeker} other {bezoekers}}",
"message.confirm-delete": "Weet je zeker dat je {target} wilt verwijderen?", "message.confirm-delete": "Weet je zeker dat je {target} wilt verwijderen?",
@ -55,6 +65,7 @@
"message.get-tracking-code": "Tracking code", "message.get-tracking-code": "Tracking code",
"message.go-to-settings": "Naar instellingen", "message.go-to-settings": "Naar instellingen",
"message.incorrect-username-password": "Incorrecte gebruikersnaam/wachtwoord.", "message.incorrect-username-password": "Incorrecte gebruikersnaam/wachtwoord.",
"message.log.visitor": "Visitor from {country} using {browser} on {os} {device}",
"message.new-version-available": "Een nieuwe versie van umami {version} is beschikbaar!", "message.new-version-available": "Een nieuwe versie van umami {version} is beschikbaar!",
"message.no-data-available": "Geen gegevens beschikbaar.", "message.no-data-available": "Geen gegevens beschikbaar.",
"message.no-websites-configured": "Je hebt geen websites ingesteld.", "message.no-websites-configured": "Je hebt geen websites ingesteld.",
@ -84,14 +95,5 @@
"metrics.referrers": "Verwijzers", "metrics.referrers": "Verwijzers",
"metrics.unique-visitors": "Unieke bezoekers", "metrics.unique-visitors": "Unieke bezoekers",
"metrics.views": "Weergaven", "metrics.views": "Weergaven",
"metrics.visitors": "Bezoekers", "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"
} }

View File

@ -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.accounts": "Contas",
"label.add-account": "Adicionar conta",
"label.add-website": "Adicionar website",
"label.administrator": "Administrador", "label.administrator": "Administrador",
"label.all": "All",
"label.all-websites": "All websites",
"label.back": "Voltar",
"label.cancel": "Cancelar",
"label.change-password": "Alterar palavra-passe",
"label.confirm-password": "Confirmar 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.current-password": "Palavra-passe atual",
"label.custom-range": "Intervalo personalizado", "label.custom-range": "Intervalo personalizado",
"label.dashboard": "Dashboard", "label.dashboard": "Dashboard",
"label.date-range": "Intervalo de datas",
"label.default-date-range": "Intervalo de datas predefinido", "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.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.enable-share-url": "Ativar link de partilha",
"label.invalid": "Inválido", "label.invalid": "Inválido",
"label.invalid-domain": "Domínio inválido", "label.invalid-domain": "Domínio inválido",
"label.last-days": "Últimos {x} dias", "label.last-days": "Últimos {x} dias",
"label.last-hours": "Últimas {x} horas", "label.last-hours": "Últimas {x} horas",
"label.logged-in-as": "Sessão iniciada como {username}", "label.logged-in-as": "Sessão iniciada como {username}",
"label.login": "Iniciar sessão",
"label.logout": "Sair", "label.logout": "Sair",
"label.more": "Mais",
"label.name": "Nome", "label.name": "Nome",
"label.new-password": "Nova palavra-passe", "label.new-password": "Nova palavra-passe",
"label.password": "Palavra-passe", "label.password": "Palavra-passe",
"label.passwords-dont-match": "Palavra-passes não correspondem", "label.passwords-dont-match": "Palavra-passes não correspondem",
"label.profile": "Perfil", "label.profile": "Perfil",
"label.realtime": "Realtime",
"label.realtime-logs": "Realtime logs",
"label.refresh": "Atualizar",
"label.required": "Obrigatório", "label.required": "Obrigatório",
"label.reset": "Repor",
"label.save": "Guardar",
"label.settings": "Definições", "label.settings": "Definições",
"label.share-url": "Partilhar link",
"label.single-day": "Dia único",
"label.this-month": "Este mês", "label.this-month": "Este mês",
"label.this-week": "Esta semana", "label.this-week": "Esta semana",
"label.this-year": "Este ano", "label.this-year": "Este ano",
"label.timezone": "Fuso horário", "label.timezone": "Fuso horário",
"label.today": "Hoje", "label.today": "Hoje",
"label.tracking-code": "Código de tracking",
"label.unknown": "Desconhecido", "label.unknown": "Desconhecido",
"label.username": "Nome de utilizador", "label.username": "Nome de utilizador",
"label.view-details": "Ver detalhes",
"label.websites": "Websites", "label.websites": "Websites",
"message.active-users": "{x} {x, plural, one {visitante} other {visitantes}} neste momento", "message.active-users": "{x} {x, plural, one {visitante} other {visitantes}} neste momento",
"message.confirm-delete": "Tens a certeza que queres eliminar {target}?", "message.confirm-delete": "Tens a certeza que queres eliminar {target}?",
@ -55,6 +65,7 @@
"message.get-tracking-code": "Obter código de tracking", "message.get-tracking-code": "Obter código de tracking",
"message.go-to-settings": "Ir para as definições", "message.go-to-settings": "Ir para as definições",
"message.incorrect-username-password": "Nome de utilizador/palavra-passe incorretos.", "message.incorrect-username-password": "Nome de utilizador/palavra-passe incorretos.",
"message.log.visitor": "Visitor from {country} using {browser} on {os} {device}",
"message.new-version-available": "Uma nova versão de umami {version} está disponível!", "message.new-version-available": "Uma nova versão de umami {version} está disponível!",
"message.no-data-available": "Sem dados disponíveis.", "message.no-data-available": "Sem dados disponíveis.",
"message.no-websites-configured": "Não tens nenhum website configurado.", "message.no-websites-configured": "Não tens nenhum website configurado.",
@ -84,14 +95,5 @@
"metrics.referrers": "Referrers", "metrics.referrers": "Referrers",
"metrics.unique-visitors": "Visitantes únicos", "metrics.unique-visitors": "Visitantes únicos",
"metrics.views": "Visualizações", "metrics.views": "Visualizações",
"metrics.visitors": "Visitantes", "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"
} }

View File

@ -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.accounts": "Conturi",
"label.add-account": "Adăugare cont",
"label.add-website": "Adăugare site web",
"label.administrator": "Administrator", "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.confirm-password": "Confirmare parolă",
"label.copy-to-clipboard": "Copiază în clipboard",
"label.current-password": "Parola curentă", "label.current-password": "Parola curentă",
"label.custom-range": "Interval personalizat", "label.custom-range": "Interval personalizat",
"label.dashboard": "Tablou de bord", "label.dashboard": "Tablou de bord",
"label.date-range": "Interval de date",
"label.default-date-range": "Interval de date implicit", "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.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.enable-share-url": "Activare adresa URL de distribuire",
"label.invalid": "Invalid", "label.invalid": "Invalid",
"label.invalid-domain": "Invalid domain", "label.invalid-domain": "Invalid domain",
"label.last-days": "Ultimele {x} zile", "label.last-days": "Ultimele {x} zile",
"label.last-hours": "Ultimele {x} ore", "label.last-hours": "Ultimele {x} ore",
"label.logged-in-as": "Autentificat ca {username}", "label.logged-in-as": "Autentificat ca {username}",
"label.login": "Autentificare",
"label.logout": "Dezautentificare", "label.logout": "Dezautentificare",
"label.more": "Mai mult",
"label.name": "Nume", "label.name": "Nume",
"label.new-password": "Parola nouă", "label.new-password": "Parola nouă",
"label.password": "Parolă", "label.password": "Parolă",
"label.passwords-dont-match": "Parolele nu se potrivesc", "label.passwords-dont-match": "Parolele nu se potrivesc",
"label.profile": "Profil", "label.profile": "Profil",
"label.realtime": "Realtime",
"label.realtime-logs": "Realtime logs",
"label.refresh": "Reîmprospătare",
"label.required": "Obligatoriu", "label.required": "Obligatoriu",
"label.reset": "Resetează",
"label.save": "Salvează",
"label.settings": "Setări", "label.settings": "Setări",
"label.share-url": "Partajare URL",
"label.single-day": "O singură zi",
"label.this-month": "Această lună", "label.this-month": "Această lună",
"label.this-week": "Această săptămână", "label.this-week": "Această săptămână",
"label.this-year": "Acest an", "label.this-year": "Acest an",
"label.timezone": "Fus orar", "label.timezone": "Fus orar",
"label.today": "Astăzi", "label.today": "Astăzi",
"label.tracking-code": "Cod de urmărire",
"label.unknown": "Necunoscut", "label.unknown": "Necunoscut",
"label.username": "Username", "label.username": "Username",
"label.view-details": "Vizualizare detalii",
"label.websites": "Site-uri web", "label.websites": "Site-uri web",
"message.active-users": "{x} {x, plural, one {vizitator activ} other {vizitatori activi}}", "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}?", "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.get-tracking-code": "Obține codul de urmărire",
"message.go-to-settings": "Mergi la Setări", "message.go-to-settings": "Mergi la Setări",
"message.incorrect-username-password": "Username/parolă incorecte.", "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.new-version-available": "Este disponibilă o nouă versiune {version} de umami!",
"message.no-data-available": "Nicio informație disponibilă.", "message.no-data-available": "Nicio informație disponibilă.",
"message.no-websites-configured": "Nu aveți niciun site web configurat.", "message.no-websites-configured": "Nu aveți niciun site web configurat.",
@ -84,14 +95,5 @@
"metrics.referrers": "Site-uri de proveniență", "metrics.referrers": "Site-uri de proveniență",
"metrics.unique-visitors": "Vizitatori unici", "metrics.unique-visitors": "Vizitatori unici",
"metrics.views": "Vizualizări", "metrics.views": "Vizualizări",
"metrics.visitors": "Vizitatori", "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"
} }

View File

@ -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.accounts": "Аккаунты",
"label.add-account": "Добавить аккаунт",
"label.add-website": "Добавить сайт",
"label.administrator": "Администратор", "label.administrator": "Администратор",
"label.all": "All",
"label.all-websites": "All websites",
"label.back": "Назад",
"label.cancel": "Отменить",
"label.change-password": "Изменить пароль",
"label.confirm-password": "Подтвердить пароль", "label.confirm-password": "Подтвердить пароль",
"label.copy-to-clipboard": "Скопировать в буфер обмена",
"label.current-password": "Текущий пароль", "label.current-password": "Текущий пароль",
"label.custom-range": "Другой период", "label.custom-range": "Другой период",
"label.dashboard": "Информационная панель", "label.dashboard": "Информационная панель",
"label.date-range": "Диапазон дат",
"label.default-date-range": "Диапазон дат по-умолчанию", "label.default-date-range": "Диапазон дат по-умолчанию",
"label.delete": "Удалить",
"label.delete-account": "Удалить аккаунт",
"label.delete-website": "Удалить сайт",
"label.dismiss": "Отклонить",
"label.domain": "Домен", "label.domain": "Домен",
"label.edit": "Редактировать",
"label.edit-account": "Редактировать аккаунт",
"label.edit-website": "Редактировать сайт",
"label.enable-share-url": "Разрешить делиться ссылкой", "label.enable-share-url": "Разрешить делиться ссылкой",
"label.invalid": "Некорректный", "label.invalid": "Некорректный",
"label.invalid-domain": "Некорректный домен", "label.invalid-domain": "Некорректный домен",
"label.last-days": "Последние {x} дней", "label.last-days": "Последние {x} дней",
"label.last-hours": "Последние {x} часа", "label.last-hours": "Последние {x} часа",
"label.logged-in-as": "Вы вошли как {username}", "label.logged-in-as": "Вы вошли как {username}",
"label.login": "Войти",
"label.logout": "Выйти", "label.logout": "Выйти",
"label.more": "Больше",
"label.name": "Имя", "label.name": "Имя",
"label.new-password": "Новый пароль", "label.new-password": "Новый пароль",
"label.password": "Пароль", "label.password": "Пароль",
"label.passwords-dont-match": "Пароли не совпадают", "label.passwords-dont-match": "Пароли не совпадают",
"label.profile": "Профиль", "label.profile": "Профиль",
"label.realtime": "Realtime",
"label.realtime-logs": "Realtime logs",
"label.refresh": "Обновить",
"label.required": "Обязательное", "label.required": "Обязательное",
"label.reset": "Сбросить",
"label.save": "Сохранить",
"label.settings": "Настройки", "label.settings": "Настройки",
"label.share-url": "Поделиться ссылкой",
"label.single-day": "Один день",
"label.this-month": "Этот месяц", "label.this-month": "Этот месяц",
"label.this-week": "Эта неделя", "label.this-week": "Эта неделя",
"label.this-year": "Этот год", "label.this-year": "Этот год",
"label.timezone": "Часовой пояс", "label.timezone": "Часовой пояс",
"label.today": "Сегодня", "label.today": "Сегодня",
"label.tracking-code": "Код отслеживания",
"label.unknown": "Неизвестно", "label.unknown": "Неизвестно",
"label.username": "Имя пользователя", "label.username": "Имя пользователя",
"label.view-details": "Посмотреть детали",
"label.websites": "Сайты", "label.websites": "Сайты",
"message.active-users": "{x} текущих посетителей", "message.active-users": "{x} текущих посетителей",
"message.confirm-delete": "Вы уверены, что хотите удалить {target}?", "message.confirm-delete": "Вы уверены, что хотите удалить {target}?",
@ -55,6 +65,7 @@
"message.get-tracking-code": "Получить код отслеживания", "message.get-tracking-code": "Получить код отслеживания",
"message.go-to-settings": "Перейти к настройкам", "message.go-to-settings": "Перейти к настройкам",
"message.incorrect-username-password": "Неверное имя пользователя/пароль.", "message.incorrect-username-password": "Неверное имя пользователя/пароль.",
"message.log.visitor": "Visitor from {country} using {browser} on {os} {device}",
"message.new-version-available": "Доступна новая версия umami {version}", "message.new-version-available": "Доступна новая версия umami {version}",
"message.no-data-available": "Нет данных.", "message.no-data-available": "Нет данных.",
"message.no-websites-configured": "У вас нет настроенных сайтов.", "message.no-websites-configured": "У вас нет настроенных сайтов.",
@ -84,14 +95,5 @@
"metrics.referrers": "Источники", "metrics.referrers": "Источники",
"metrics.unique-visitors": "Уникальные посетители", "metrics.unique-visitors": "Уникальные посетители",
"metrics.views": "Просмотры", "metrics.views": "Просмотры",
"metrics.visitors": "Посетители", "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": "Код отслеживания"
} }

View File

@ -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.accounts": "Konton",
"label.add-account": "Lägg till konto",
"label.add-website": "Lägg till webbsajt",
"label.administrator": "Administratör", "label.administrator": "Administratör",
"label.all": "All",
"label.all-websites": "All websites",
"label.back": "Tillbaka",
"label.cancel": "Avbryt",
"label.change-password": "Byt lösenord",
"label.confirm-password": "Bekräfta lösenord", "label.confirm-password": "Bekräfta lösenord",
"label.copy-to-clipboard": "Kopiera till urklipp",
"label.current-password": "Nuvarande lösenord", "label.current-password": "Nuvarande lösenord",
"label.custom-range": "Anpassat urval", "label.custom-range": "Anpassat urval",
"label.dashboard": "Instrumentpanel", "label.dashboard": "Instrumentpanel",
"label.date-range": "Datumomfång",
"label.default-date-range": "Standard datum-urval", "label.default-date-range": "Standard datum-urval",
"label.delete": "Radera",
"label.delete-account": "Radera konto",
"label.delete-website": "Radera webbsajt",
"label.dismiss": "Dismiss",
"label.domain": "Domän", "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.enable-share-url": "Aktivera delnings-URL",
"label.invalid": "Ogiltig", "label.invalid": "Ogiltig",
"label.invalid-domain": "Ogiltig domän", "label.invalid-domain": "Ogiltig domän",
"label.last-days": "Senaste {x} dagarna", "label.last-days": "Senaste {x} dagarna",
"label.last-hours": "Senaste {x} timmarna", "label.last-hours": "Senaste {x} timmarna",
"label.logged-in-as": "Inloggad som {username}", "label.logged-in-as": "Inloggad som {username}",
"label.login": "Logga in",
"label.logout": "Logga ut", "label.logout": "Logga ut",
"label.more": "Mer",
"label.name": "Namn", "label.name": "Namn",
"label.new-password": "Nytt lösenord", "label.new-password": "Nytt lösenord",
"label.password": "Lösenord", "label.password": "Lösenord",
"label.passwords-dont-match": "Lösenorden är inte samma", "label.passwords-dont-match": "Lösenorden är inte samma",
"label.profile": "Profil", "label.profile": "Profil",
"label.realtime": "Realtime",
"label.realtime-logs": "Realtime logs",
"label.refresh": "Uppdatera",
"label.required": "Krävs", "label.required": "Krävs",
"label.reset": "Återställ",
"label.save": "Spara",
"label.settings": "Inställningar", "label.settings": "Inställningar",
"label.share-url": "Delnings-URL",
"label.single-day": "En dag",
"label.this-month": "Denna månad", "label.this-month": "Denna månad",
"label.this-week": "Denna vecka", "label.this-week": "Denna vecka",
"label.this-year": "Detta år", "label.this-year": "Detta år",
"label.timezone": "Tidszon", "label.timezone": "Tidszon",
"label.today": "Idag", "label.today": "Idag",
"label.tracking-code": "Spårningskod",
"label.unknown": "Okänd", "label.unknown": "Okänd",
"label.username": "Användarnamn", "label.username": "Användarnamn",
"label.view-details": "Visa detaljer",
"label.websites": "Webbsajt", "label.websites": "Webbsajt",
"message.active-users": "{x} {x, plural, one {besökare} other {besökare}} just nu", "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}?", "message.confirm-delete": "Är du säker på att du vill radera {target}?",
@ -55,6 +65,7 @@
"message.get-tracking-code": "Visa spårningskod", "message.get-tracking-code": "Visa spårningskod",
"message.go-to-settings": "Gå till inställningar", "message.go-to-settings": "Gå till inställningar",
"message.incorrect-username-password": "Felaktikt användarnamn/lösenord.", "message.incorrect-username-password": "Felaktikt användarnamn/lösenord.",
"message.log.visitor": "Visitor from {country} using {browser} on {os} {device}",
"message.new-version-available": "A new version of umami {version} is available!", "message.new-version-available": "A new version of umami {version} is available!",
"message.no-data-available": "Ingen data tillgänglig.", "message.no-data-available": "Ingen data tillgänglig.",
"message.no-websites-configured": "Du har inga webbsajter.", "message.no-websites-configured": "Du har inga webbsajter.",
@ -84,14 +95,5 @@
"metrics.referrers": "Hänvisare", "metrics.referrers": "Hänvisare",
"metrics.unique-visitors": "Unika besökare", "metrics.unique-visitors": "Unika besökare",
"metrics.views": "Visningar", "metrics.views": "Visningar",
"metrics.visitors": "Besökare", "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"
} }

View File

@ -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.accounts": "Hesaplar",
"label.add-account": "Hesap ekle",
"label.add-website": "Web sitesi ekle",
"label.administrator": "Yönetici", "label.administrator": "Yönetici",
"label.all": "All",
"label.all-websites": "All websites",
"label.back": "Geri",
"label.cancel": "İptal",
"label.change-password": "Şifre değiştir",
"label.confirm-password": "Parolayı onayla", "label.confirm-password": "Parolayı onayla",
"label.copy-to-clipboard": "Panoya kopyala",
"label.current-password": "Mevcut parola", "label.current-password": "Mevcut parola",
"label.custom-range": "Özelleştirilmiş aralık", "label.custom-range": "Özelleştirilmiş aralık",
"label.dashboard": "Kontrol Paneli", "label.dashboard": "Kontrol Paneli",
"label.date-range": "Tarih aralığı",
"label.default-date-range": "Varsayılan 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": "Dismiss",
"label.domain": "Alan adı", "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.enable-share-url": "Anonim paylaşım URL'i aktif",
"label.invalid": "Geçeriz", "label.invalid": "Geçeriz",
"label.invalid-domain": "Geçersiz alan adı", "label.invalid-domain": "Geçersiz alan adı",
"label.last-days": "Son {x} gün", "label.last-days": "Son {x} gün",
"label.last-hours": "Son {x} saat", "label.last-hours": "Son {x} saat",
"label.logged-in-as": "{username} olarak giriş yapıldı.", "label.logged-in-as": "{username} olarak giriş yapıldı.",
"label.login": "Giriş Yap",
"label.logout": ıkış Yap", "label.logout": ıkış Yap",
"label.more": "Detaylı göster",
"label.name": "İsim", "label.name": "İsim",
"label.new-password": "Yeni parola", "label.new-password": "Yeni parola",
"label.password": "Parola", "label.password": "Parola",
"label.passwords-dont-match": "Parolalar uyuşmuyor", "label.passwords-dont-match": "Parolalar uyuşmuyor",
"label.profile": "Profil", "label.profile": "Profil",
"label.realtime": "Realtime",
"label.realtime-logs": "Realtime logs",
"label.refresh": "Yenile",
"label.required": "Zorunlu alan", "label.required": "Zorunlu alan",
"label.reset": "Sıfırla",
"label.save": "Kaydet",
"label.settings": "Ayarlar", "label.settings": "Ayarlar",
"label.share-url": "Paylaşım adresi",
"label.single-day": "Tekil gün",
"label.this-month": "Bu ay", "label.this-month": "Bu ay",
"label.this-week": "Bu hafta", "label.this-week": "Bu hafta",
"label.this-year": "Bu yıl", "label.this-year": "Bu yıl",
"label.timezone": "Zaman dilimi", "label.timezone": "Zaman dilimi",
"label.today": "Bugün", "label.today": "Bugün",
"label.tracking-code": "İzleme kodu",
"label.unknown": "Bilinmeyen", "label.unknown": "Bilinmeyen",
"label.username": "Kullanıcı adı", "label.username": "Kullanıcı adı",
"label.view-details": "Detayı incele",
"label.websites": "Web siteleri", "label.websites": "Web siteleri",
"message.active-users": "{x} aktif ziyaretçi", "message.active-users": "{x} aktif ziyaretçi",
"message.confirm-delete": "{target} kaydını silmek istediğinizden emin misiniz?", "message.confirm-delete": "{target} kaydını silmek istediğinizden emin misiniz?",
@ -55,6 +65,7 @@
"message.get-tracking-code": "İzleme kodunu al", "message.get-tracking-code": "İzleme kodunu al",
"message.go-to-settings": "Ayarlara git", "message.go-to-settings": "Ayarlara git",
"message.incorrect-username-password": "Hatalı kullanıcı adı ya da parola.", "message.incorrect-username-password": "Hatalı kullanıcı adı ya da parola.",
"message.log.visitor": "Visitor from {country} using {browser} on {os} {device}",
"message.new-version-available": "A new version of umami {version} is available!", "message.new-version-available": "A new version of umami {version} is available!",
"message.no-data-available": "Henüz hiç veri yok.", "message.no-data-available": "Henüz hiç veri yok.",
"message.no-websites-configured": "Henüz hiç web sitesi tanımlamadınız", "message.no-websites-configured": "Henüz hiç web sitesi tanımlamadınız",
@ -84,14 +95,5 @@
"metrics.referrers": "Yönlendirenler", "metrics.referrers": "Yönlendirenler",
"metrics.unique-visitors": "Tekil kullanıcı", "metrics.unique-visitors": "Tekil kullanıcı",
"metrics.views": "Görüntüleme", "metrics.views": "Görüntüleme",
"metrics.visitors": "Ziyaretçi", "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"
} }

View File

@ -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.accounts": "Облікові записи",
"label.add-account": "Додати обліковий запис",
"label.add-website": "Додати website",
"label.administrator": "Адміністратор", "label.administrator": "Адміністратор",
"label.all": "All",
"label.all-websites": "All websites",
"label.back": "Назад",
"label.cancel": "Відмінити",
"label.change-password": "Змінити пароль",
"label.confirm-password": "Підтвердити пароль", "label.confirm-password": "Підтвердити пароль",
"label.copy-to-clipboard": "Копіювати до буферу обміну",
"label.current-password": "Поточний пароль", "label.current-password": "Поточний пароль",
"label.custom-range": "Довільний період", "label.custom-range": "Довільний період",
"label.dashboard": "Інформаційна панель", "label.dashboard": "Інформаційна панель",
"label.date-range": "Діапазон дат",
"label.default-date-range": "Діапазон дат за умовчанням", "label.default-date-range": "Діапазон дат за умовчанням",
"label.delete": "Видалити",
"label.delete-account": "Видалити обліковий запис",
"label.delete-website": "Видалити веб-сайт",
"label.dismiss": "Відхилити",
"label.domain": "Домен", "label.domain": "Домен",
"label.edit": "Редагувати",
"label.edit-account": "Редагувати обліковий запис",
"label.edit-website": "Редагувати веб-сайт",
"label.enable-share-url": "Дозволити ділитися посиланням", "label.enable-share-url": "Дозволити ділитися посиланням",
"label.invalid": "Некоректний", "label.invalid": "Некоректний",
"label.invalid-domain": "Некоректний домен", "label.invalid-domain": "Некоректний домен",
"label.last-days": "Останні {x} днів", "label.last-days": "Останні {x} днів",
"label.last-hours": "Останні {x} годин", "label.last-hours": "Останні {x} годин",
"label.logged-in-as": "Ви увійшли як {username}", "label.logged-in-as": "Ви увійшли як {username}",
"label.login": "Увійти",
"label.logout": "Вийти", "label.logout": "Вийти",
"label.more": "Більше",
"label.name": "Ім'я", "label.name": "Ім'я",
"label.new-password": "Новий пароль", "label.new-password": "Новий пароль",
"label.password": "Пароль", "label.password": "Пароль",
"label.passwords-dont-match": "Паролі не співпадають", "label.passwords-dont-match": "Паролі не співпадають",
"label.profile": "Профіль", "label.profile": "Профіль",
"label.realtime": "Realtime",
"label.realtime-logs": "Realtime logs",
"label.refresh": "Оновити",
"label.required": "Обов'язкове", "label.required": "Обов'язкове",
"label.reset": "Скинути",
"label.save": "Зберегти",
"label.settings": "Налаштування", "label.settings": "Налаштування",
"label.share-url": "Поділитися посилання",
"label.single-day": "Один день",
"label.this-month": "Поточний місяць", "label.this-month": "Поточний місяць",
"label.this-week": "Поточний тиждень", "label.this-week": "Поточний тиждень",
"label.this-year": "Поточний рік", "label.this-year": "Поточний рік",
"label.timezone": "Часовий пояс", "label.timezone": "Часовий пояс",
"label.today": "Сьогодні", "label.today": "Сьогодні",
"label.tracking-code": "Код для відслідковування",
"label.unknown": "Невідомо", "label.unknown": "Невідомо",
"label.username": "Ім'я користувача", "label.username": "Ім'я користувача",
"label.view-details": "Переглянути деталі",
"label.websites": "Веб-сайти", "label.websites": "Веб-сайти",
"message.active-users": "{x} поточних відвідувачів", "message.active-users": "{x} поточних відвідувачів",
"message.confirm-delete": "Ви впевнені, що бажаєте видалити {target}?", "message.confirm-delete": "Ви впевнені, що бажаєте видалити {target}?",
@ -55,6 +65,7 @@
"message.get-tracking-code": "Отримати код для відслідковування", "message.get-tracking-code": "Отримати код для відслідковування",
"message.go-to-settings": "Перейти до налаштувань", "message.go-to-settings": "Перейти до налаштувань",
"message.incorrect-username-password": "Невірне ім'я користувача або пароль.", "message.incorrect-username-password": "Невірне ім'я користувача або пароль.",
"message.log.visitor": "Visitor from {country} using {browser} on {os} {device}",
"message.new-version-available": "Нова версія umami {version} доступна!", "message.new-version-available": "Нова версія umami {version} доступна!",
"message.no-data-available": "Немає даних.", "message.no-data-available": "Немає даних.",
"message.no-websites-configured": "У вас немає налаштованих веб-сайтів.", "message.no-websites-configured": "У вас немає налаштованих веб-сайтів.",
@ -84,14 +95,5 @@
"metrics.referrers": "Джерела", "metrics.referrers": "Джерела",
"metrics.unique-visitors": "Унікальні відвідувачі", "metrics.unique-visitors": "Унікальні відвідувачі",
"metrics.views": "Перегляди", "metrics.views": "Перегляди",
"metrics.visitors": "Відвідувачі", "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": "Код для відслідковування"
} }

View File

@ -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.accounts": "账户",
"label.add-account": "添加账户",
"label.add-website": "添加网站",
"label.administrator": "管理员", "label.administrator": "管理员",
"label.all": "All",
"label.all-websites": "All websites",
"label.back": "返回",
"label.cancel": "取消",
"label.change-password": "更新密码",
"label.confirm-password": "确认密码", "label.confirm-password": "确认密码",
"label.copy-to-clipboard": "复制",
"label.current-password": "目前密码", "label.current-password": "目前密码",
"label.custom-range": "自定义时间段", "label.custom-range": "自定义时间段",
"label.dashboard": "仪表板", "label.dashboard": "仪表板",
"label.date-range": "多日",
"label.default-date-range": "默认日期范围", "label.default-date-range": "默认日期范围",
"label.delete": "删除",
"label.delete-account": "删除账户",
"label.delete-website": "删除网站",
"label.dismiss": "Dismiss",
"label.domain": "域名", "label.domain": "域名",
"label.edit": "编辑",
"label.edit-account": "编辑账户",
"label.edit-website": "编辑网站",
"label.enable-share-url": "激活共享链接", "label.enable-share-url": "激活共享链接",
"label.invalid": "输入无效", "label.invalid": "输入无效",
"label.invalid-domain": "无效域名", "label.invalid-domain": "无效域名",
"label.last-days": "最近 {x} 天", "label.last-days": "最近 {x} 天",
"label.last-hours": "最近 {x} 小时", "label.last-hours": "最近 {x} 小时",
"label.logged-in-as": "登录名: {username}", "label.logged-in-as": "登录名: {username}",
"label.login": "登录",
"label.logout": "退出", "label.logout": "退出",
"label.more": "更多",
"label.name": "名字", "label.name": "名字",
"label.new-password": "新密码", "label.new-password": "新密码",
"label.password": "密码", "label.password": "密码",
"label.passwords-dont-match": "密码不一致", "label.passwords-dont-match": "密码不一致",
"label.profile": "个人资料", "label.profile": "个人资料",
"label.realtime": "Realtime",
"label.realtime-logs": "Realtime logs",
"label.refresh": "刷新",
"label.required": "必填", "label.required": "必填",
"label.reset": "重置",
"label.save": "保存",
"label.settings": "设置", "label.settings": "设置",
"label.share-url": "共享链接",
"label.single-day": "单日",
"label.this-month": "本月", "label.this-month": "本月",
"label.this-week": "本周", "label.this-week": "本周",
"label.this-year": "今年", "label.this-year": "今年",
"label.timezone": "时区", "label.timezone": "时区",
"label.today": "今天", "label.today": "今天",
"label.tracking-code": "跟踪代码",
"label.unknown": "未知", "label.unknown": "未知",
"label.username": "用户名", "label.username": "用户名",
"label.view-details": "查看更多",
"label.websites": "网站", "label.websites": "网站",
"message.active-users": "当前在线 {x} 人", "message.active-users": "当前在线 {x} 人",
"message.confirm-delete": "你确定要删除{target}吗?", "message.confirm-delete": "你确定要删除{target}吗?",
@ -55,6 +65,7 @@
"message.get-tracking-code": "获得跟踪代码", "message.get-tracking-code": "获得跟踪代码",
"message.go-to-settings": "去设置", "message.go-to-settings": "去设置",
"message.incorrect-username-password": "用户名密码不正确.", "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.new-version-available": "A new version of umami {version} is available!",
"message.no-data-available": "无可用数据.", "message.no-data-available": "无可用数据.",
"message.no-websites-configured": "你还没有设置任何网站.", "message.no-websites-configured": "你还没有设置任何网站.",
@ -84,14 +95,5 @@
"metrics.referrers": "指入域名", "metrics.referrers": "指入域名",
"metrics.unique-visitors": "独立访客", "metrics.unique-visitors": "独立访客",
"metrics.views": "页面流量", "metrics.views": "页面流量",
"metrics.visitors": "独立访客", "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": "跟踪代码"
} }

View File

@ -1,6 +1,6 @@
import { parse } from 'cookie'; import { parse } from 'cookie';
import { parseSecureToken, parseToken } from './crypto'; import { parseSecureToken, parseToken } from './crypto';
import { AUTH_COOKIE_NAME } from './constants'; import { AUTH_COOKIE_NAME, TOKEN_HEADER } from './constants';
import { getWebsiteById } from './queries'; import { getWebsiteById } from './queries';
export async function getAuthToken(req) { export async function getAuthToken(req) {
@ -26,13 +26,14 @@ export async function isValidToken(token, validation) {
} }
export async function allowQuery(req, skipToken) { export async function allowQuery(req, skipToken) {
const { id, token } = req.query; const { id } = req.query;
const token = req.headers[TOKEN_HEADER];
const websiteId = +id; const websiteId = +id;
const website = await getWebsiteById(websiteId); const website = await getWebsiteById(websiteId);
if (website) { if (website) {
if (token && !skipToken) { if (token && token !== 'undefined' && !skipToken) {
return isValidToken(token, { website_id: websiteId }); return isValidToken(token, { website_id: websiteId });
} }

View File

@ -4,6 +4,16 @@ export const TIMEZONE_CONFIG = 'umami.timezone';
export const DATE_RANGE_CONFIG = 'umami.date-range'; export const DATE_RANGE_CONFIG = 'umami.date-range';
export const THEME_CONFIG = 'umami.theme'; export const THEME_CONFIG = 'umami.theme';
export const VERSION_CHECK = 'umami.version-check'; export const VERSION_CHECK = 'umami.version-check';
export const TOKEN_HEADER = 'x-umami-token';
export const DEFAULT_LOCALE = 'en-US';
export const DEFAULT_THEME = 'light';
export const DEFAUL_CHART_HEIGHT = 400;
export const DEFAULT_ANIMATION_DURATION = 300;
export const DEFAULT_DATE_RANGE = '24hour';
export const REALTIME_RANGE = 30;
export const REALTIME_INTERVAL = 3000;
export const THEME_COLORS = { export const THEME_COLORS = {
light: { light: {
@ -42,13 +52,15 @@ export const EVENT_COLORS = [
'#44b556', '#44b556',
'#e68619', '#e68619',
'#e34850', '#e34850',
'#1b959a', '#f7bd12',
'#d83790', '#01bad7',
'#85d044', '#6734bc',
'#89c541',
'#ffc301',
'#ec1562',
'#ffec16',
]; ];
export const DEFAULT_DATE_RANGE = '24hour';
export const POSTGRESQL = 'postgresql'; export const POSTGRESQL = 'postgresql';
export const MYSQL = 'mysql'; export const MYSQL = 'mysql';
@ -68,10 +80,6 @@ export const POSTGRESQL_DATE_FORMATS = {
year: 'YYYY-01-01', year: 'YYYY-01-01',
}; };
export const FILTER_DOMAIN_ONLY = 0;
export const FILTER_COMBINED = 1;
export const FILTER_RAW = 2;
export const DOMAIN_REGEX = /localhost(:\d{1,5})?|((?=[a-z0-9-]{1,63}\.)(xn--)?[a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{2,63}/; export const DOMAIN_REGEX = /localhost(:\d{1,5})?|((?=[a-z0-9-]{1,63}\.)(xn--)?[a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{2,63}/;
export const DESKTOP_SCREEN_WIDTH = 1920; export const DESKTOP_SCREEN_WIDTH = 1920;

View File

@ -7,6 +7,7 @@ import {
addYears, addYears,
subHours, subHours,
subDays, subDays,
startOfMinute,
startOfHour, startOfHour,
startOfDay, startOfDay,
startOfWeek, startOfWeek,
@ -17,6 +18,7 @@ import {
endOfWeek, endOfWeek,
endOfMonth, endOfMonth,
endOfYear, endOfYear,
differenceInMinutes,
differenceInHours, differenceInHours,
differenceInCalendarDays, differenceInCalendarDays,
differenceInCalendarMonths, differenceInCalendarMonths,
@ -114,6 +116,7 @@ export function getDateFromString(str) {
} }
const dateFuncs = { const dateFuncs = {
minute: [differenceInMinutes, addMinutes, startOfMinute],
hour: [differenceInHours, addHours, startOfHour], hour: [differenceInHours, addHours, startOfHour],
day: [differenceInCalendarDays, addDays, startOfDay], day: [differenceInCalendarDays, addDays, startOfDay],
month: [differenceInCalendarMonths, addMonths, startOfMonth], month: [differenceInCalendarMonths, addMonths, startOfMonth],

View File

@ -62,3 +62,19 @@ export function formatLongNumber(value) {
return formatNumber(n); return formatNumber(n);
} }
export function stringToColor(str) {
if (!str) {
return '#ffffff';
}
let hash = 0;
for (let i = 0; i < str.length; i++) {
hash = str.charCodeAt(i) + ((hash << 5) - hash);
}
let color = '#';
for (let i = 0; i < 3; i++) {
let value = (hash >> (i * 8)) & 0xff;
color += ('00' + value.toString(16)).substr(-2);
}
return color;
}

View File

@ -17,6 +17,7 @@ import {
nb, nb,
id, id,
uk, uk,
fi,
} from 'date-fns/locale'; } from 'date-fns/locale';
import enMessages from 'lang-compiled/en-US.json'; import enMessages from 'lang-compiled/en-US.json';
import nlMessages from 'lang-compiled/nl-NL.json'; import nlMessages from 'lang-compiled/nl-NL.json';
@ -37,6 +38,7 @@ import roMessages from 'lang-compiled/ro-RO.json';
import nbNOMessages from 'lang-compiled/nb-NO.json'; import nbNOMessages from 'lang-compiled/nb-NO.json';
import idMessages from 'lang-compiled/id-ID.json'; import idMessages from 'lang-compiled/id-ID.json';
import ukMessages from 'lang-compiled/uk-UA.json'; import ukMessages from 'lang-compiled/uk-UA.json';
import fiMessages from 'lang-compiled/fi-FI.json';
export const messages = { export const messages = {
'en-US': enMessages, 'en-US': enMessages,
@ -58,6 +60,7 @@ export const messages = {
'nb-NO': nbNOMessages, 'nb-NO': nbNOMessages,
'id-ID': idMessages, 'id-ID': idMessages,
'uk-UA': ukMessages, 'uk-UA': ukMessages,
'fi-FI': fiMessages,
}; };
export const dateLocales = { export const dateLocales = {
@ -80,6 +83,7 @@ export const dateLocales = {
'nb-NO': nb, 'nb-NO': nb,
'id-ID': id, 'id-ID': id,
'uk-UA': uk, 'uk-UA': uk,
'fi-FI': fi,
}; };
export const menuOptions = [ export const menuOptions = [
@ -99,6 +103,7 @@ export const menuOptions = [
{ label: 'Português', value: 'pt-PT', display: 'pt' }, { label: 'Português', value: 'pt-PT', display: 'pt' },
{ label: 'Русский', value: 'ru-RU', display: 'ru' }, { label: 'Русский', value: 'ru-RU', display: 'ru' },
{ label: 'Română', value: 'ro-RO', display: 'ro' }, { label: 'Română', value: 'ro-RO', display: 'ro' },
{ label: 'Suomi', value: 'fi-FI', display: 'fi' },
{ label: 'Svenska', value: 'sv-SE', display: 'sv' }, { label: 'Svenska', value: 'sv-SE', display: 'sv' },
{ label: 'Türkçe', value: 'tr-TR', display: 'tr' }, { label: 'Türkçe', value: 'tr-TR', display: 'tr' },
{ label: 'українська', value: 'uk-UA', display: 'uk' }, { label: 'українська', value: 'uk-UA', display: 'uk' },

View File

@ -166,16 +166,6 @@ export async function createSession(website_id, data) {
); );
} }
export async function getSessionById(session_id) {
return runQuery(
prisma.session.findOne({
where: {
session_id,
},
}),
);
}
export async function getSessionByUuid(session_uuid) { export async function getSessionByUuid(session_uuid) {
return runQuery( return runQuery(
prisma.session.findOne({ prisma.session.findOne({
@ -285,7 +275,58 @@ export async function createAccount(data) {
); );
} }
export function getMetrics(website_id, start_at, end_at, filters = {}) { export async function getSessions(websites, start_at) {
return runQuery(
prisma.session.findMany({
where: {
website: {
website_id: {
in: websites,
},
},
created_at: {
gte: start_at,
},
},
}),
);
}
export async function getPageviews(websites, start_at) {
return runQuery(
prisma.pageview.findMany({
where: {
website: {
website_id: {
in: websites,
},
},
created_at: {
gte: start_at,
},
},
}),
);
}
export async function getEvents(websites, start_at) {
return runQuery(
prisma.event.findMany({
where: {
website: {
website_id: {
in: websites,
},
},
created_at: {
gte: start_at,
},
},
}),
);
}
export function getWebsiteStats(website_id, start_at, end_at, filters = {}) {
const params = [website_id, start_at, end_at]; const params = [website_id, start_at, end_at];
const { url } = filters; const { url } = filters;
let urlFilter = ''; let urlFilter = '';
@ -317,7 +358,7 @@ export function getMetrics(website_id, start_at, end_at, filters = {}) {
); );
} }
export function getPageviews( export function getPageviewStats(
website_id, website_id,
start_at, start_at,
end_at, end_at,
@ -425,7 +466,7 @@ export function getActiveVisitors(website_id) {
); );
} }
export function getEvents( export function getEventMetrics(
website_id, website_id,
start_at, start_at,
end_at, end_at,
@ -459,3 +500,30 @@ export function getEvents(
params, params,
); );
} }
export async function getRealtimeData(websites, time) {
const [pageviews, sessions, events] = await Promise.all([
getPageviews(websites, time),
getSessions(websites, time),
getEvents(websites, time),
]);
return {
pageviews: pageviews.map(({ view_id, ...props }) => ({
__id: `p${view_id}`,
view_id,
...props,
})),
sessions: sessions.map(({ session_id, ...props }) => ({
__id: `s${session_id}`,
session_id,
...props,
})),
events: events.map(({ event_id, ...props }) => ({
__id: `e${event_id}`,
event_id,
...props,
})),
timestamp: Date.now(),
};
}

View File

@ -28,3 +28,7 @@ export function getQueryString(params = {}) {
return ''; return '';
} }
export function makeUrl(url, params) {
return `${url}${getQueryString(params)}`;
}

View File

@ -1,4 +1,4 @@
import { getQueryString } from './url'; import { makeUrl } from './url';
export const apiRequest = (method, url, body, headers) => export const apiRequest = (method, url, body, headers) =>
fetch(url, { fetch(url, {
@ -19,13 +19,17 @@ export const apiRequest = (method, url, body, headers) =>
return res.text().then(data => ({ ok: res.ok, status: res.status, res: res, data })); return res.text().then(data => ({ ok: res.ok, status: res.status, res: res, data }));
}); });
export const get = (url, params) => apiRequest('get', `${url}${getQueryString(params)}`); export const get = (url, params, headers) =>
apiRequest('get', makeUrl(url, params), undefined, headers);
export const del = (url, params) => apiRequest('delete', `${url}${getQueryString(params)}`); export const del = (url, params, headers) =>
apiRequest('delete', makeUrl(url, params), undefined, headers);
export const post = (url, params) => apiRequest('post', url, JSON.stringify(params)); export const post = (url, params, headers) =>
apiRequest('post', url, JSON.stringify(params), headers);
export const put = (url, params) => apiRequest('put', url, JSON.stringify(params)); export const put = (url, params, headers) =>
apiRequest('put', url, JSON.stringify(params), headers);
export const hook = (_this, method, callback) => { export const hook = (_this, method, callback) => {
const orig = _this[method]; const orig = _this[method];

View File

@ -1,6 +1,6 @@
{ {
"name": "umami", "name": "umami",
"version": "0.80.0", "version": "0.96.0",
"description": "A simple, fast, website analytics alternative to Google Analytics. ", "description": "A simple, fast, website analytics alternative to Google Analytics. ",
"author": "Mike Cao <mike@mikecao.com>", "author": "Mike Cao <mike@mikecao.com>",
"license": "MIT", "license": "MIT",
@ -56,7 +56,7 @@
} }
}, },
"dependencies": { "dependencies": {
"@prisma/client": "2.8.0", "@prisma/client": "2.9.0",
"@reduxjs/toolkit": "^1.4.0", "@reduxjs/toolkit": "^1.4.0",
"bcrypt": "^5.0.0", "bcrypt": "^5.0.0",
"chalk": "^4.1.0", "chalk": "^4.1.0",
@ -65,17 +65,17 @@
"cookie": "^0.4.1", "cookie": "^0.4.1",
"cors": "^2.8.5", "cors": "^2.8.5",
"date-fns": "^2.16.1", "date-fns": "^2.16.1",
"date-fns-tz": "^1.0.10", "date-fns-tz": "^1.0.12",
"detect-browser": "^5.1.1", "detect-browser": "^5.2.0",
"dotenv": "^8.2.0", "dotenv": "^8.2.0",
"formik": "^2.1.7", "formik": "^2.2.0",
"immer": "^7.0.9", "immer": "^7.0.9",
"is-localhost-ip": "^1.4.0", "is-localhost-ip": "^1.4.0",
"isbot-fast": "^1.2.0", "isbot-fast": "^1.2.0",
"jose": "^2.0.2", "jose": "^2.0.2",
"maxmind": "^4.2.0", "maxmind": "^4.3.0",
"moment-timezone": "^0.5.31", "moment-timezone": "^0.5.31",
"next": "^9.5.4", "next": "^9.5.5",
"react": "^16.13.1", "react": "^16.13.1",
"react-dom": "^16.13.1", "react-dom": "^16.13.1",
"react-intl": "^5.8.4", "react-intl": "^5.8.4",
@ -83,6 +83,7 @@
"react-simple-maps": "^2.1.2", "react-simple-maps": "^2.1.2",
"react-spring": "^8.0.27", "react-spring": "^8.0.27",
"react-tooltip": "^4.2.10", "react-tooltip": "^4.2.10",
"react-use-measure": "^2.0.2",
"react-window": "^1.8.5", "react-window": "^1.8.5",
"redux": "^4.0.5", "redux": "^4.0.5",
"redux-thunk": "^2.3.0", "redux-thunk": "^2.3.0",
@ -91,11 +92,11 @@
"thenby": "^1.3.4", "thenby": "^1.3.4",
"timezone-support": "^2.0.2", "timezone-support": "^2.0.2",
"tinycolor2": "^1.4.2", "tinycolor2": "^1.4.2",
"uuid": "^8.3.0" "uuid": "^8.3.1"
}, },
"devDependencies": { "devDependencies": {
"@formatjs/cli": "^2.13.0", "@formatjs/cli": "^2.13.2",
"@prisma/cli": "2.8.0", "@prisma/cli": "2.9.0",
"@rollup/plugin-buble": "^0.21.3", "@rollup/plugin-buble": "^0.21.3",
"@rollup/plugin-node-resolve": "^9.0.0", "@rollup/plugin-node-resolve": "^9.0.0",
"@rollup/plugin-replace": "^2.3.3", "@rollup/plugin-replace": "^2.3.3",
@ -103,10 +104,10 @@
"cross-env": "^7.0.2", "cross-env": "^7.0.2",
"del": "^6.0.0", "del": "^6.0.0",
"dotenv-cli": "^4.0.0", "dotenv-cli": "^4.0.0",
"eslint": "^7.10.0", "eslint": "^7.11.0",
"eslint-config-prettier": "^6.12.0", "eslint-config-prettier": "^6.12.0",
"eslint-plugin-prettier": "^3.1.3", "eslint-plugin-prettier": "^3.1.3",
"eslint-plugin-react": "^7.21.2", "eslint-plugin-react": "^7.21.4",
"eslint-plugin-react-hooks": "^4.1.2", "eslint-plugin-react-hooks": "^4.1.2",
"extract-react-intl-messages": "^4.1.1", "extract-react-intl-messages": "^4.1.1",
"husky": "^4.3.0", "husky": "^4.3.0",
@ -118,7 +119,7 @@
"postcss-preset-env": "^6.7.0", "postcss-preset-env": "^6.7.0",
"prettier": "^2.1.2", "prettier": "^2.1.2",
"prettier-eslint": "^11.0.0", "prettier-eslint": "^11.0.0",
"rollup": "^2.28.2", "rollup": "^2.30.0",
"rollup-plugin-hashbang": "^2.2.2", "rollup-plugin-hashbang": "^2.2.2",
"rollup-plugin-terser": "^7.0.2", "rollup-plugin-terser": "^7.0.2",
"stylelint": "^13.7.2", "stylelint": "^13.7.2",

View File

@ -7,7 +7,7 @@ import { getIpAddress } from '../../lib/request';
export default async (req, res) => { export default async (req, res) => {
await useCors(req, res); await useCors(req, res);
if (isBot(req.headers['user-agent'])) { if (isBot(req.headers['user-agent'])) {
return ok(res); return ok(res);
} }

View File

@ -0,0 +1,26 @@
import { subMinutes } from 'date-fns';
import { useAuth } from 'lib/middleware';
import { ok, methodNotAllowed } from 'lib/response';
import { getUserWebsites, getRealtimeData } from 'lib/queries';
import { createToken } from 'lib/crypto';
export default async (req, res) => {
await useAuth(req, res);
if (req.method === 'GET') {
const { user_id } = req.auth;
const websites = await getUserWebsites(user_id);
const ids = websites.map(({ website_id }) => website_id);
const token = await createToken({ websites: ids });
const data = await getRealtimeData(ids, subMinutes(new Date(), 30));
return ok(res, {
websites,
token,
data,
});
}
return methodNotAllowed(res);
};

View File

@ -0,0 +1,27 @@
import { useAuth } from 'lib/middleware';
import { ok, methodNotAllowed, badRequest } from 'lib/response';
import { getRealtimeData } from 'lib/queries';
import { parseToken } from 'lib/crypto';
import { TOKEN_HEADER } from 'lib/constants';
export default async (req, res) => {
await useAuth(req, res);
if (req.method === 'GET') {
const { start_at } = req.query;
const token = req.headers[TOKEN_HEADER];
if (!token) {
return badRequest(res);
}
const { websites } = await parseToken(token);
const data = await getRealtimeData(websites, new Date(+start_at));
return ok(res, data);
}
return methodNotAllowed(res);
};

View File

@ -1,5 +1,5 @@
import moment from 'moment-timezone'; import moment from 'moment-timezone';
import { getEvents } from 'lib/queries'; import { getEventMetrics } from 'lib/queries';
import { ok, badRequest, methodNotAllowed, unauthorized } from 'lib/response'; import { ok, badRequest, methodNotAllowed, unauthorized } from 'lib/response';
import { allowQuery } from 'lib/auth'; import { allowQuery } from 'lib/auth';
@ -21,7 +21,7 @@ export default async (req, res) => {
const startDate = new Date(+start_at); const startDate = new Date(+start_at);
const endDate = new Date(+end_at); const endDate = new Date(+end_at);
const events = await getEvents(websiteId, startDate, endDate, tz, unit, { url }); const events = await getEventMetrics(websiteId, startDate, endDate, tz, unit, { url });
return ok(res, events); return ok(res, events);
} }

View File

@ -1,27 +1,67 @@
import { getMetrics } from 'lib/queries'; import { getPageviewMetrics, getSessionMetrics } from 'lib/queries';
import { methodNotAllowed, ok, unauthorized } from 'lib/response'; import { ok, badRequest, methodNotAllowed, unauthorized } from 'lib/response';
import { DOMAIN_REGEX } from 'lib/constants';
import { allowQuery } from 'lib/auth'; import { allowQuery } from 'lib/auth';
const sessionColumns = ['browser', 'os', 'device', 'country'];
const pageviewColumns = ['url', 'referrer'];
function getTable(type) {
if (type === 'event') {
return 'event';
}
if (sessionColumns.includes(type)) {
return 'session';
}
return 'pageview';
}
function getColumn(type) {
if (type === 'event') {
return `concat(event_type, ':', event_value)`;
}
return type;
}
export default async (req, res) => { export default async (req, res) => {
if (req.method === 'GET') { if (req.method === 'GET') {
if (!(await allowQuery(req))) { if (!(await allowQuery(req))) {
return unauthorized(res); return unauthorized(res);
} }
const { id, start_at, end_at, url } = req.query; const { id, type, start_at, end_at, domain, url } = req.query;
if (domain && !DOMAIN_REGEX.test(domain)) {
return badRequest(res);
}
const websiteId = +id; const websiteId = +id;
const startDate = new Date(+start_at); const startDate = new Date(+start_at);
const endDate = new Date(+end_at); const endDate = new Date(+end_at);
const metrics = await getMetrics(websiteId, startDate, endDate, { url }); if (sessionColumns.includes(type)) {
const data = await getSessionMetrics(websiteId, startDate, endDate, type, { url });
const stats = Object.keys(metrics[0]).reduce((obj, key) => { return ok(res, data);
obj[key] = Number(metrics[0][key]) || 0; }
return obj;
}, {});
return ok(res, stats); if (type === 'event' || pageviewColumns.includes(type)) {
const data = await getPageviewMetrics(
websiteId,
startDate,
endDate,
getColumn(type),
getTable(type),
{
domain: type !== 'event' && domain,
url: type !== 'url' && url,
},
);
return ok(res, data);
}
} }
return methodNotAllowed(res); return methodNotAllowed(res);

View File

@ -1,5 +1,5 @@
import moment from 'moment-timezone'; import moment from 'moment-timezone';
import { getPageviews } from 'lib/queries'; import { getPageviewStats } from 'lib/queries';
import { ok, badRequest, methodNotAllowed, unauthorized } from 'lib/response'; import { ok, badRequest, methodNotAllowed, unauthorized } from 'lib/response';
import { allowQuery } from 'lib/auth'; import { allowQuery } from 'lib/auth';
@ -21,12 +21,12 @@ export default async (req, res) => {
return badRequest(res); return badRequest(res);
} }
const [pageviews, uniques] = await Promise.all([ const [pageviews, sessions] = await Promise.all([
getPageviews(websiteId, startDate, endDate, tz, unit, '*', url), getPageviewStats(websiteId, startDate, endDate, tz, unit, '*', url),
getPageviews(websiteId, startDate, endDate, tz, unit, 'distinct session_id', url), getPageviewStats(websiteId, startDate, endDate, tz, unit, 'distinct session_id', url),
]); ]);
return ok(res, { pageviews, uniques }); return ok(res, { pageviews, sessions });
} }
return methodNotAllowed(res); return methodNotAllowed(res);

Some files were not shown because too many files have changed in this diff Show More