Merge remote-tracking branch 'upstream/master'

pull/584/head
karthikpasupathy 2021-03-28 09:00:47 +05:30
commit 9069cb830e
147 changed files with 4689 additions and 3795 deletions

1
Procfile Normal file
View File

@ -0,0 +1 @@
web: npm run start-env

View File

@ -1,10 +1,11 @@
import React from 'react';
import PropTypes from 'prop-types';
import ReactTooltip from 'react-tooltip';
import classNames from 'classnames';
import Icon from './Icon';
import styles from './Button.module.css';
export default function Button({
function Button({
type = 'button',
icon,
size,
@ -43,3 +44,19 @@ export default function Button({
</button>
);
}
Button.propTypes = {
type: PropTypes.oneOf(['button', 'submit', 'reset']),
icon: PropTypes.node,
size: PropTypes.oneOf(['xlarge', 'large', 'medium', 'small', 'xsmall']),
variant: PropTypes.oneOf(['action', 'danger', 'light']),
children: PropTypes.node,
className: PropTypes.string,
tooltip: PropTypes.node,
tooltipId: PropTypes.string,
disabled: PropTypes.bool,
iconRight: PropTypes.bool,
onClick: PropTypes.func,
};
export default Button;

View File

@ -1,16 +1,10 @@
import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import Button from './Button';
import styles from './ButtonGroup.module.css';
export default function ButtonGroup({
items = [],
selectedItem,
className,
size,
icon,
onClick = () => {},
}) {
function ButtonGroup({ items = [], selectedItem, className, size, icon, onClick = () => {} }) {
return (
<div className={classNames(styles.group, className)}>
{items.map(item => {
@ -30,3 +24,19 @@ export default function ButtonGroup({
</div>
);
}
ButtonGroup.propTypes = {
items: PropTypes.arrayOf(
PropTypes.shape({
label: PropTypes.node,
value: PropTypes.any.isRequired,
}),
),
selectedItem: PropTypes.any,
className: PropTypes.string,
size: PropTypes.oneOf(['xlarge', 'large', 'medium', 'small', 'xsmall']),
icon: PropTypes.node,
onClick: PropTypes.func,
};
export default ButtonGroup;

View File

@ -18,8 +18,9 @@ import {
} from 'date-fns';
import Button from './Button';
import useLocale from 'hooks/useLocale';
import { dateFormat } from 'lib/lang';
import { dateFormat } from 'lib/date';
import { chunk } from 'lib/array';
import { dateLocales } from 'lib/lang';
import Chevron from 'assets/chevron-down.svg';
import Cross from 'assets/times.svg';
import styles from './Calendar.module.css';
@ -105,8 +106,8 @@ export default function Calendar({ date, minDate, maxDate, onChange }) {
}
const DaySelector = ({ date, minDate, maxDate, locale, onSelect }) => {
const startWeek = startOfWeek(date);
const startMonth = startOfMonth(date);
const startWeek = startOfWeek(date, { locale: dateLocales[locale] });
const startMonth = startOfMonth(date, { locale: dateLocales[locale] });
const startDay = subDays(startMonth, startMonth.getDay());
const month = date.getMonth();
const year = date.getFullYear();

View File

@ -1,9 +1,10 @@
import React, { useRef } from 'react';
import PropTypes from 'prop-types';
import Icon from 'components/common/Icon';
import Check from 'assets/check.svg';
import styles from './Checkbox.module.css';
export default function Checkbox({ name, value, label, onChange }) {
function Checkbox({ name, value, label, onChange }) {
const ref = useRef();
return (
@ -25,3 +26,12 @@ export default function Checkbox({ name, value, label, onChange }) {
</div>
);
}
Checkbox.propTypes = {
name: PropTypes.string,
value: PropTypes.any,
label: PropTypes.node,
onChange: PropTypes.func,
};
export default Checkbox;

View File

@ -1,4 +1,5 @@
import React, { useState } from 'react';
import PropTypes from 'prop-types';
import Button from './Button';
import { FormattedMessage } from 'react-intl';
@ -6,7 +7,7 @@ const defaultText = (
<FormattedMessage id="label.copy-to-clipboard" defaultMessage="Copy to clipboard" />
);
export default function CopyButton({ element, ...props }) {
function CopyButton({ element, ...props }) {
const [text, setText] = useState(defaultText);
function handleClick() {
@ -24,3 +25,13 @@ export default function CopyButton({ element, ...props }) {
</Button>
);
}
CopyButton.propTypes = {
element: PropTypes.shape({
current: PropTypes.shape({
select: PropTypes.func.isRequired,
}),
}),
};
export default CopyButton;

View File

@ -1,12 +1,12 @@
import React, { useState } from 'react';
import PropTypes from 'prop-types';
import { FormattedMessage } from 'react-intl';
import { endOfYear, isSameDay } from 'date-fns';
import Modal from './Modal';
import DropDown from './DropDown';
import DatePickerForm from 'components/forms/DatePickerForm';
import useLocale from 'hooks/useLocale';
import { getDateRange } from 'lib/date';
import { dateFormat } from 'lib/lang';
import { getDateRange, dateFormat } from 'lib/date';
import Calendar from 'assets/calendar-alt.svg';
import Icon from './Icon';
@ -54,7 +54,8 @@ const filterOptions = [
},
];
export default function DateFilter({ value, startDate, endDate, onChange, className }) {
function DateFilter({ value, startDate, endDate, onChange, className }) {
const [locale] = useLocale();
const [showPicker, setShowPicker] = useState(false);
const displayValue =
value === 'custom' ? (
@ -68,7 +69,7 @@ export default function DateFilter({ value, startDate, endDate, onChange, classN
setShowPicker(true);
return;
}
onChange(getDateRange(value));
onChange(getDateRange(value, locale));
}
function handlePickerChange(value) {
@ -117,3 +118,13 @@ const CustomRange = ({ startDate, endDate, onClick }) => {
</>
);
};
DateFilter.propTypes = {
value: PropTypes.string,
startDate: PropTypes.instanceOf(Date),
endDate: PropTypes.instanceOf(Date),
onChange: PropTypes.func,
className: PropTypes.string,
};
export default DateFilter;

View File

@ -1,8 +1,9 @@
import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import styles from './Dot.module.css';
export default function Dot({ color, size, className }) {
function Dot({ color, size, className }) {
return (
<div className={styles.wrapper}>
<div
@ -15,3 +16,11 @@ export default function Dot({ color, size, className }) {
</div>
);
}
Dot.propTypes = {
color: PropTypes.string,
size: PropTypes.oneOf(['small', 'large']),
className: PropTypes.string,
};
export default Dot;

View File

@ -1,4 +1,5 @@
import React, { useState, useRef } from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import Menu from './Menu';
import useDocumentClick from 'hooks/useDocumentClick';
@ -6,13 +7,7 @@ import Chevron from 'assets/chevron-down.svg';
import styles from './Dropdown.module.css';
import Icon from './Icon';
export default function DropDown({
value,
className,
menuClassName,
options = [],
onChange = () => {},
}) {
function DropDown({ value, className, menuClassName, options = [], onChange = () => {} }) {
const [showMenu, setShowMenu] = useState(false);
const ref = useRef();
const selectedOption = options.find(e => e.value === value);
@ -29,7 +24,7 @@ export default function DropDown({
}
useDocumentClick(e => {
if (!ref.current.contains(e.target)) {
if (!ref.current?.contains(e.target)) {
setShowMenu(false);
}
});
@ -52,3 +47,18 @@ export default function DropDown({
</div>
);
}
DropDown.propTypes = {
value: PropTypes.any,
className: PropTypes.string,
menuClassName: PropTypes.string,
options: PropTypes.arrayOf(
PropTypes.shape({
value: PropTypes.any.isRequired,
label: PropTypes.node,
}),
),
onChange: PropTypes.func,
};
export default DropDown;

View File

@ -1,14 +1,22 @@
import React from 'react';
import PropTypes from 'prop-types';
import Icon from 'components/common/Icon';
import Logo from 'assets/logo.svg';
import styles from './EmptyPlaceholder.module.css';
export default function EmptyPlaceholder({ msg, children }) {
function EmptyPlaceholder({ msg, children }) {
return (
<div className={styles.placeholder}>
<Icon className={styles.icon} icon={<Logo />} size="xlarge" />
<h2>{msg}</h2>
<h2 className={styles.msg}>{msg}</h2>
{children}
</div>
);
}
EmptyPlaceholder.propTypes = {
msg: PropTypes.node,
children: PropTypes.node,
};
export default EmptyPlaceholder;

View File

@ -9,3 +9,7 @@
.icon {
margin-bottom: 30px;
}
.msg {
margin-bottom: 15px;
}

View File

@ -1,4 +1,5 @@
import React from 'react';
import PropTypes from 'prop-types';
import styles from './Favicon.module.css';
function getHostName(url) {
@ -6,7 +7,7 @@ function getHostName(url) {
return match && match.length > 1 ? match[1] : null;
}
export default function Favicon({ domain, ...props }) {
function Favicon({ domain, ...props }) {
const hostName = domain ? getHostName(domain) : null;
return hostName ? (
@ -19,3 +20,9 @@ export default function Favicon({ domain, ...props }) {
/>
) : null;
}
Favicon.propTypes = {
domain: PropTypes.string,
};
export default Favicon;

View File

@ -1,11 +1,25 @@
import React from 'react';
import PropTypes from 'prop-types';
import ButtonLayout from 'components/layout/ButtonLayout';
import ButtonGroup from './ButtonGroup';
export default function FilterButtons({ buttons, selected, onClick }) {
function FilterButtons({ buttons, selected, onClick }) {
return (
<ButtonLayout>
<ButtonGroup size="xsmall" items={buttons} selectedItem={selected} onClick={onClick} />
</ButtonLayout>
);
}
FilterButtons.propTypes = {
buttons: PropTypes.arrayOf(
PropTypes.shape({
label: PropTypes.node,
value: PropTypes.any.isRequired,
}),
),
selected: PropTypes.any,
onClick: PropTypes.func,
};
export default FilterButtons;

View File

@ -1,8 +1,9 @@
import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import styles from './Icon.module.css';
export default function Icon({ icon, className, size = 'medium', ...props }) {
function Icon({ icon, className, size = 'medium', ...props }) {
return (
<div
className={classNames(styles.icon, className, {
@ -18,3 +19,11 @@ export default function Icon({ icon, className, size = 'medium', ...props }) {
</div>
);
}
Icon.propTypes = {
className: PropTypes.string,
icon: PropTypes.node.isRequired,
size: PropTypes.oneOf(['xlarge', 'large', 'medium', 'small', 'xsmall']),
};
export default Icon;

View File

@ -1,10 +1,11 @@
import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import NextLink from 'next/link';
import Icon from './Icon';
import styles from './Link.module.css';
export default function Link({ className, icon, children, size, iconRight, ...props }) {
function Link({ className, icon, children, size, iconRight, ...props }) {
return (
<NextLink {...props}>
<a
@ -21,3 +22,13 @@ export default function Link({ className, icon, children, size, iconRight, ...pr
</NextLink>
);
}
Link.propTypes = {
className: PropTypes.string,
icon: PropTypes.node,
children: PropTypes.node,
size: PropTypes.oneOf(['large', 'small', 'xsmall']),
iconRight: PropTypes.bool,
};
export default Link;

View File

@ -1,8 +1,9 @@
import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import styles from './Loading.module.css';
export default function Loading({ className }) {
function Loading({ className }) {
return (
<div className={classNames(styles.loading, className)}>
<div />
@ -11,3 +12,9 @@ export default function Loading({ className }) {
</div>
);
}
Loading.propTypes = {
className: PropTypes.string,
};
export default Loading;

View File

@ -1,8 +1,9 @@
import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import styles from './Menu.module.css';
export default function Menu({
function Menu({
options = [],
selectedOption,
className,
@ -46,3 +47,24 @@ export default function Menu({
</div>
);
}
Menu.propTypes = {
options: PropTypes.arrayOf(
PropTypes.shape({
label: PropTypes.node,
value: PropTypes.any,
className: PropTypes.string,
render: PropTypes.func,
divider: PropTypes.bool,
}),
),
selectedOption: PropTypes.any,
className: PropTypes.string,
float: PropTypes.oneOf(['top', 'bottom']),
align: PropTypes.oneOf(['left', 'right']),
optionClassName: PropTypes.string,
selectedClassName: PropTypes.string,
onSelect: PropTypes.func,
};
export default Menu;

View File

@ -1,11 +1,12 @@
import React, { useState, useRef } from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import Menu from 'components/common/Menu';
import Button from 'components/common/Button';
import useDocumentClick from 'hooks/useDocumentClick';
import styles from './MenuButton.module.css';
export default function MenuButton({
function MenuButton({
icon,
value,
options,
@ -30,7 +31,7 @@ export default function MenuButton({
}
useDocumentClick(e => {
if (!ref.current.contains(e.target)) {
if (!ref.current?.contains(e.target)) {
setShowMenu(false);
}
});
@ -58,3 +59,25 @@ export default function MenuButton({
</div>
);
}
MenuButton.propTypes = {
icon: PropTypes.node,
value: PropTypes.any,
options: PropTypes.arrayOf(
PropTypes.shape({
label: PropTypes.node,
value: PropTypes.any,
className: PropTypes.string,
render: PropTypes.func,
divider: PropTypes.bool,
}),
),
buttonClassName: PropTypes.string,
menuClassName: PropTypes.string,
menuPosition: PropTypes.oneOf(['top', 'bottom']),
menuAlign: PropTypes.oneOf(['left', 'right']),
onSelect: PropTypes.func,
renderValue: PropTypes.func,
};
export default MenuButton;

View File

@ -1,9 +1,10 @@
import React from 'react';
import PropTypes from 'prop-types';
import ReactDOM from 'react-dom';
import { useSpring, animated } from 'react-spring';
import styles from './Modal.module.css';
export default function Modal({ title, children }) {
function Modal({ title, children }) {
const props = useSpring({ opacity: 1, from: { opacity: 0 } });
return ReactDOM.createPortal(
@ -16,3 +17,10 @@ export default function Modal({ title, children }) {
document.getElementById('__modals'),
);
}
Modal.propTypes = {
title: PropTypes.node,
children: PropTypes.node,
};
export default Modal;

View File

@ -1,9 +1,10 @@
import React from 'react';
import PropTypes from 'prop-types';
import { useRouter } from 'next/router';
import classNames from 'classnames';
import styles from './NavMenu.module.css';
export default function NavMenu({ options = [], className, onSelect = () => {} }) {
function NavMenu({ options = [], className, onSelect = () => {} }) {
const router = useRouter();
return (
@ -30,3 +31,17 @@ export default function NavMenu({ options = [], className, onSelect = () => {} }
</div>
);
}
NavMenu.propTypes = {
options: PropTypes.arrayOf(
PropTypes.shape({
label: PropTypes.node,
value: PropTypes.any,
className: PropTypes.string,
render: PropTypes.func,
}),
),
className: PropTypes.string,
onSelect: PropTypes.func,
};
export default NavMenu;

View File

@ -1,12 +1,19 @@
import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import { FormattedMessage } from 'react-intl';
import styles from './NoData.module.css';
export default function NoData({ className }) {
function NoData({ className }) {
return (
<div className={classNames(styles.container, className)}>
<FormattedMessage id="message.no-data-available" defaultMessage="No data available." />
</div>
);
}
NoData.propTypes = {
className: PropTypes.string,
};
export default NoData;

View File

@ -1,8 +1,11 @@
.container {
color: var(--gray500);
font-size: var(--font-size-normal);
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
position: relative;
display: flex;
align-items: center;
justify-content: center;
text-align: center;
width: 100%;
height: 100%;
}

View File

@ -1,4 +1,5 @@
import React, { useState, useEffect } from 'react';
import PropTypes from 'prop-types';
import { useDispatch, useSelector } from 'react-redux';
import { FormattedMessage } from 'react-intl';
import { setDateRange } from 'redux/actions/websites';
@ -7,9 +8,11 @@ import Refresh from 'assets/redo.svg';
import Dots from 'assets/ellipsis-h.svg';
import useDateRange from 'hooks/useDateRange';
import { getDateRange } from '../../lib/date';
import useLocale from 'hooks/useLocale';
export default function RefreshButton({ websiteId }) {
function RefreshButton({ websiteId }) {
const dispatch = useDispatch();
const [locale] = useLocale();
const [dateRange] = useDateRange(websiteId);
const [loading, setLoading] = useState(false);
const completed = useSelector(state => state.queries[`/api/website/${websiteId}/stats`]);
@ -17,7 +20,7 @@ export default function RefreshButton({ websiteId }) {
function handleClick() {
if (dateRange) {
setLoading(true);
dispatch(setDateRange(websiteId, getDateRange(dateRange.value)));
dispatch(setDateRange(websiteId, getDateRange(dateRange.value, locale)));
}
}
@ -35,3 +38,9 @@ export default function RefreshButton({ websiteId }) {
/>
);
}
RefreshButton.propTypes = {
websiteId: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
};
export default RefreshButton;

View File

@ -1,9 +1,10 @@
import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import NoData from 'components/common/NoData';
import styles from './Table.module.css';
export default function Table({
function Table({
columns,
rows,
empty,
@ -45,6 +46,34 @@ export default function Table({
);
}
const styledObject = PropTypes.shape({
className: PropTypes.string,
style: PropTypes.object,
});
Table.propTypes = {
columns: PropTypes.arrayOf(
PropTypes.shape({
cell: styledObject,
className: PropTypes.string,
header: styledObject,
key: PropTypes.string,
label: PropTypes.node,
render: PropTypes.func,
style: PropTypes.object,
}),
),
rows: PropTypes.arrayOf(PropTypes.object),
empty: PropTypes.node,
className: PropTypes.string,
bodyClassName: PropTypes.string,
rowKey: PropTypes.func,
showHeader: PropTypes.bool,
children: PropTypes.node,
};
export default Table;
export const TableRow = ({ columns, row }) => (
<div className={classNames(styles.row, 'row')}>
{columns.map(({ key, render, className, style, cell }, index) => (

View File

@ -1,7 +1,15 @@
import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import styles from './Tag.module.css';
export default function Tag({ className, children }) {
function Tag({ className, children }) {
return <span className={classNames(styles.tag, className)}>{children}</span>;
}
Tag.propTypes = {
className: PropTypes.string,
children: PropTypes.node,
};
export default Tag;

View File

@ -1,11 +1,12 @@
import React, { useEffect } from 'react';
import PropTypes from 'prop-types';
import ReactDOM from 'react-dom';
import { useSpring, animated } from 'react-spring';
import styles from './Toast.module.css';
import Icon from 'components/common/Icon';
import Close from 'assets/times.svg';
export default function Toast({ message, timeout = 3000, onClose }) {
function Toast({ message, timeout = 3000, onClose }) {
const props = useSpring({
opacity: 1,
transform: 'translate3d(0,0px,0)',
@ -24,3 +25,11 @@ export default function Toast({ message, timeout = 3000, onClose }) {
document.getElementById('__modals'),
);
}
Toast.propTypes = {
message: PropTypes.node,
timeout: PropTypes.number,
onClose: PropTypes.func,
};
export default Toast;

View File

@ -1,17 +1,18 @@
import React, { useState, useMemo } from 'react';
import { useRouter } from 'next/router';
import PropTypes from 'prop-types';
import ReactTooltip from 'react-tooltip';
import { ComposableMap, Geographies, Geography, ZoomableGroup } from 'react-simple-maps';
import classNames from 'classnames';
import tinycolor from 'tinycolor2';
import useTheme from 'hooks/useTheme';
import { THEME_COLORS } from 'lib/constants';
import { ISO_COUNTRIES, THEME_COLORS, MAP_FILE } from 'lib/constants';
import styles from './WorldMap.module.css';
import useCountryNames from 'hooks/useCountryNames';
import useLocale from 'hooks/useLocale';
const geoUrl = '/world-110m.json';
export default function WorldMap({ data, className }) {
function WorldMap({ data, className }) {
const { basePath } = useRouter();
const [tooltip, setTooltip] = useState();
const [theme] = useTheme();
const colors = useMemo(
@ -57,10 +58,10 @@ export default function WorldMap({ data, className }) {
>
<ComposableMap projection="geoMercator">
<ZoomableGroup zoom={0.8} minZoom={0.7} center={[0, 40]}>
<Geographies geography={geoUrl}>
<Geographies geography={`${basePath}${MAP_FILE}`}>
{({ geographies }) => {
return geographies.map(geo => {
const code = geo.properties.ISO_A2;
const code = ISO_COUNTRIES[geo.id];
return (
<Geography
@ -87,3 +88,16 @@ export default function WorldMap({ data, className }) {
</div>
);
}
WorldMap.propTypes = {
data: PropTypes.arrayOf(
PropTypes.shape({
x: PropTypes.string,
y: PropTypes.number,
z: PropTypes.number,
}),
),
className: PropTypes.string,
};
export default WorldMap;

View File

@ -1,8 +1,6 @@
import React, { useState } from 'react';
import { FormattedMessage } from 'react-intl';
import { Formik, Form, Field } from 'formik';
import { useRouter } from 'next/router';
import { post } from 'lib/web';
import Button from 'components/common/Button';
import FormLayout, {
FormButtons,
@ -10,6 +8,7 @@ import FormLayout, {
FormMessage,
FormRow,
} from 'components/layout/FormLayout';
import usePost from 'hooks/usePost';
const initialValues = {
username: '',
@ -30,11 +29,11 @@ const validate = ({ user_id, username, password }) => {
};
export default function AccountEditForm({ values, onSave, onClose }) {
const { basePath } = useRouter();
const post = usePost();
const [message, setMessage] = useState();
const handleSubmit = async values => {
const { ok, data } = await post(`${basePath}/api/account`, values);
const { ok, data } = await post('/api/account', values);
if (ok) {
onSave();
@ -58,15 +57,19 @@ export default function AccountEditForm({ values, onSave, onClose }) {
<label htmlFor="username">
<FormattedMessage id="label.username" defaultMessage="Username" />
</label>
<Field name="username" type="text" />
<FormError name="username" />
<div>
<Field name="username" type="text" />
<FormError name="username" />
</div>
</FormRow>
<FormRow>
<label htmlFor="password">
<FormattedMessage id="label.password" defaultMessage="Password" />
</label>
<Field name="password" type="password" />
<FormError name="password" />
<div>
<Field name="password" type="password" />
<FormError name="password" />
</div>
</FormRow>
<FormButtons>
<Button type="submit" variant="action">

View File

@ -1,8 +1,6 @@
import React, { useState } from 'react';
import { FormattedMessage } from 'react-intl';
import { useRouter } from 'next/router';
import { Formik, Form, Field } from 'formik';
import { post } from 'lib/web';
import Button from 'components/common/Button';
import FormLayout, {
FormButtons,
@ -10,6 +8,7 @@ import FormLayout, {
FormMessage,
FormRow,
} from 'components/layout/FormLayout';
import usePost from 'hooks/usePost';
const initialValues = {
current_password: '',
@ -38,11 +37,11 @@ const validate = ({ current_password, new_password, confirm_password }) => {
};
export default function ChangePasswordForm({ values, onSave, onClose }) {
const { basePath } = useRouter();
const post = usePost();
const [message, setMessage] = useState();
const handleSubmit = async values => {
const { ok, data } = await post(`${basePath}/api/account/password`, values);
const { ok, data } = await post('/api/account/password', values);
if (ok) {
onSave();
@ -66,22 +65,28 @@ export default function ChangePasswordForm({ values, onSave, onClose }) {
<label htmlFor="current_password">
<FormattedMessage id="label.current-password" defaultMessage="Current password" />
</label>
<Field name="current_password" type="password" />
<FormError name="current_password" />
<div>
<Field name="current_password" type="password" />
<FormError name="current_password" />
</div>
</FormRow>
<FormRow>
<label htmlFor="new_password">
<FormattedMessage id="label.new-password" defaultMessage="New password" />
</label>
<Field name="new_password" type="password" />
<FormError name="new_password" />
<div>
<Field name="new_password" type="password" />
<FormError name="new_password" />
</div>
</FormRow>
<FormRow>
<label htmlFor="confirm_password">
<FormattedMessage id="label.confirm-password" defaultMessage="Confirm password" />
</label>
<Field name="confirm_password" type="password" />
<FormError name="confirm_password" />
<div>
<Field name="confirm_password" type="password" />
<FormError name="confirm_password" />
</div>
</FormRow>
<FormButtons>
<Button type="submit" variant="action">

View File

@ -1,8 +1,6 @@
import React, { useState } from 'react';
import { FormattedMessage } from 'react-intl';
import { useRouter } from 'next/router';
import { Formik, Form, Field } from 'formik';
import { del } from 'lib/web';
import Button from 'components/common/Button';
import FormLayout, {
FormButtons,
@ -10,6 +8,7 @@ import FormLayout, {
FormMessage,
FormRow,
} from 'components/layout/FormLayout';
import useDelete from 'hooks/useDelete';
const CONFIRMATION_WORD = 'DELETE';
@ -28,11 +27,11 @@ const validate = ({ confirmation }) => {
};
export default function DeleteForm({ values, onSave, onClose }) {
const { basePath } = useRouter();
const del = useDelete();
const [message, setMessage] = useState();
const handleSubmit = async ({ type, id }) => {
const { ok, data } = await del(`${basePath}/api/${type}/${id}`);
const { ok, data } = await del(`/api/${type}/${id}`);
if (ok) {
onSave();
@ -73,8 +72,10 @@ export default function DeleteForm({ values, onSave, onClose }) {
/>
</p>
<FormRow>
<Field name="confirmation" type="text" />
<FormError name="confirmation" />
<div>
<Field name="confirmation" type="text" />
<FormError name="confirmation" />
</div>
</FormRow>
<FormButtons>
<Button

View File

@ -2,7 +2,6 @@ import React, { useState } from 'react';
import { FormattedMessage } from 'react-intl';
import { Formik, Form, Field } from 'formik';
import { useRouter } from 'next/router';
import { post } from 'lib/web';
import Button from 'components/common/Button';
import FormLayout, {
FormButtons,
@ -13,6 +12,7 @@ import FormLayout, {
import Icon from 'components/common/Icon';
import Logo from 'assets/logo.svg';
import styles from './LoginForm.module.css';
import usePost from 'hooks/usePost';
const validate = ({ username, password }) => {
const errors = {};
@ -28,11 +28,12 @@ const validate = ({ username, password }) => {
};
export default function LoginForm() {
const post = usePost();
const router = useRouter();
const [message, setMessage] = useState();
const handleSubmit = async ({ username, password }) => {
const { ok, status, data } = await post(`${router.basePath}/api/auth/login`, {
const { ok, status, data } = await post('/api/auth/login', {
username,
password,
});
@ -65,21 +66,27 @@ export default function LoginForm() {
>
{() => (
<Form>
<Icon icon={<Logo />} size="xlarge" className={styles.icon} />
<h1 className="center">umami</h1>
<div className={styles.header}>
<Icon icon={<Logo />} size="xlarge" className={styles.icon} />
<h1 className="center">umami</h1>
</div>
<FormRow>
<label htmlFor="username">
<FormattedMessage id="label.username" defaultMessage="Username" />
</label>
<Field name="username" type="text" />
<FormError name="username" />
<div>
<Field name="username" type="text" />
<FormError name="username" />
</div>
</FormRow>
<FormRow>
<label htmlFor="password">
<FormattedMessage id="label.password" defaultMessage="Password" />
</label>
<Field name="password" type="password" />
<FormError name="password" />
<div>
<Field name="password" type="password" />
<FormError name="password" />
</div>
</FormRow>
<FormButtons>
<Button type="submit" variant="action">

View File

@ -13,3 +13,11 @@
justify-content: center;
margin: 0 auto;
}
.header {
margin-bottom: 30px;
}
.header h1 {
margin: 12px 0;
}

View File

@ -1,11 +1,13 @@
import React, { useRef } from 'react';
import { FormattedMessage } from 'react-intl';
import { useRouter } from 'next/router';
import Button from 'components/common/Button';
import FormLayout, { FormButtons, FormRow } from 'components/layout/FormLayout';
import CopyButton from 'components/common/CopyButton';
export default function TrackingCodeForm({ values, onClose }) {
const ref = useRef();
const { basePath } = useRouter();
const { name, share_id } = values;
return (
@ -23,7 +25,9 @@ export default function TrackingCodeForm({ values, onClose }) {
rows={3}
cols={60}
spellCheck={false}
defaultValue={`${document.location.origin}/share/${share_id}/${encodeURIComponent(name)}`}
defaultValue={`${
document.location.origin
}${basePath}/share/${share_id}/${encodeURIComponent(name)}`}
readOnly
/>
</FormRow>

View File

@ -1,11 +1,13 @@
import React, { useRef } from 'react';
import { FormattedMessage } from 'react-intl';
import { useRouter } from 'next/router';
import Button from 'components/common/Button';
import FormLayout, { FormButtons, FormRow } from 'components/layout/FormLayout';
import CopyButton from 'components/common/CopyButton';
export default function TrackingCodeForm({ values, onClose }) {
const ref = useRef();
const { basePath } = useRouter();
return (
<FormLayout>
@ -22,7 +24,7 @@ export default function TrackingCodeForm({ values, onClose }) {
rows={3}
cols={60}
spellCheck={false}
defaultValue={`<script async defer data-website-id="${values.website_uuid}" src="${document.location.origin}/umami.js"></script>`}
defaultValue={`<script async defer data-website-id="${values.website_uuid}" src="${document.location.origin}${basePath}/umami.js"></script>`}
readOnly
/>
</FormRow>

View File

@ -1,7 +1,6 @@
import React, { useState } from 'react';
import { FormattedMessage } from 'react-intl';
import { Formik, Form, Field } from 'formik';
import { post } from 'lib/web';
import Button from 'components/common/Button';
import FormLayout, {
FormButtons,
@ -11,7 +10,7 @@ import FormLayout, {
} from 'components/layout/FormLayout';
import Checkbox from 'components/common/Checkbox';
import { DOMAIN_REGEX } from 'lib/constants';
import { useRouter } from 'next/router';
import usePost from 'hooks/usePost';
const initialValues = {
name: '',
@ -35,11 +34,11 @@ const validate = ({ name, domain }) => {
};
export default function WebsiteEditForm({ values, onSave, onClose }) {
const { basePath } = useRouter();
const post = usePost();
const [message, setMessage] = useState();
const handleSubmit = async values => {
const { ok, data } = await post(`${basePath}/api/website`, values);
const { ok, data } = await post('/api/website', values);
if (ok) {
onSave();
@ -63,15 +62,19 @@ export default function WebsiteEditForm({ values, onSave, onClose }) {
<label htmlFor="name">
<FormattedMessage id="label.name" defaultMessage="Name" />
</label>
<Field name="name" type="text" />
<FormError name="name" />
<div>
<Field name="name" type="text" />
<FormError name="name" />
</div>
</FormRow>
<FormRow>
<label htmlFor="domain">
<FormattedMessage id="label.domain" defaultMessage="Domain" />
</label>
<Field name="domain" type="text" />
<FormError name="domain" />
<div>
<Field name="domain" type="text" placeholder="example.com" />
<FormError name="domain" />
</div>
</FormRow>
<FormRow>
<label></label>

View File

@ -11,10 +11,22 @@
.row {
display: flex;
flex-wrap: wrap;
align-items: center;
position: relative;
margin-bottom: 20px;
line-height: 1.8;
padding: 0 20px;
}
.row > div {
position: relative;
flex: 1 1;
}
.row > div > input {
width: 100%;
min-width: 240px;
}
.buttons {
@ -33,9 +45,9 @@
justify-content: center;
align-items: center;
top: 0;
left: 100%;
left: calc(100% + 16px);
bottom: 0;
margin-left: 16px;
z-index: 1;
}
.msg {
@ -68,3 +80,15 @@
color: var(--gray50);
background: var(--gray800);
}
@media only screen and (max-width: 576px) {
.error {
align-items: flex-start;
top: calc(100% + 7px);
left: 0;
}
.error:after {
left: 10px;
}
}

View File

@ -35,6 +35,6 @@
.row > .col {
border-top: 1px solid var(--gray300);
border-left: 0;
padding: 0;
padding: 20px 0;
}
}

View File

@ -1,4 +1,4 @@
import React from 'react';
import React, { useState } from 'react';
import { FormattedMessage } from 'react-intl';
import { useSelector } from 'react-redux';
import classNames from 'classnames';
@ -13,40 +13,66 @@ import styles from './Header.module.css';
export default function Header() {
const user = useSelector(state => state.user);
const [active, setActive] = useState(false);
function handleClick() {
setActive(state => !state);
}
return (
<header className="container">
<nav className="container">
{user?.is_admin && <UpdateNotice />}
<div className={classNames(styles.header, 'row align-items-center')}>
<div className="col-12 col-md-12 col-lg-3">
<div className={styles.title}>
<Icon icon={<Logo />} size="large" className={styles.logo} />
<Link href={user ? '/' : 'https://umami.is'}>umami</Link>
<div className={styles.nav}>
<div className="">
<div className={styles.title}>
<Icon icon={<Logo />} size="large" className={styles.logo} />
<Link href={user ? '/' : 'https://umami.is'}>umami</Link>
</div>
</div>
</div>
<div className="col-12 col-md-12 col-lg-6">
<button
onClick={handleClick}
role="button"
className={styles.burger}
aria-label="menu"
aria-expanded="false"
>
{active ? (
<div> X </div>
) : (
<>
<span></span>
<span></span>
<span></span>
</>
)}
</button>
{user && (
<div className={styles.nav}>
<Link href="/dashboard">
<FormattedMessage id="label.dashboard" defaultMessage="Dashboard" />
</Link>
<Link href="/realtime">
<FormattedMessage id="label.realtime" defaultMessage="Realtime" />
</Link>
<Link href="/settings">
<FormattedMessage id="label.settings" defaultMessage="Settings" />
</Link>
<div className={styles.items}>
<div className={active ? classNames(styles.active) : ''}>
<Link href="/dashboard">
<FormattedMessage id="label.dashboard" defaultMessage="Dashboard" />
</Link>
<Link href="/realtime">
<FormattedMessage id="label.realtime" defaultMessage="Realtime" />
</Link>
<Link href="/settings">
<FormattedMessage id="label.settings" defaultMessage="Settings" />
</Link>
</div>
</div>
)}
</div>
<div className="col-12 col-md-12 col-lg-3">
<div className={styles.buttons}>
<ThemeButton />
<LanguageButton menuAlign="right" />
{user && <UserButton />}
<div className={styles.items}>
<div className={active ? classNames(styles.active) : ''}>
<div className={styles.buttons}>
<ThemeButton />
<LanguageButton menuAlign="right" />
{user && <UserButton />}
</div>
</div>
</div>
</div>
</div>
</header>
</nav>
);
}

View File

@ -1,6 +1,19 @@
.navbar {
align-items: stretch;
display: flex;
flex-wrap: wrap;
justify-content: space-between;
width: 100%;
}
.burger {
display: none;
}
.header {
display: flex;
min-height: 100px;
width: 100%;
}
.title {
@ -15,6 +28,15 @@
}
.nav {
display: flex;
align-items: center;
font-size: var(--font-size-normal);
font-weight: 600;
width: 100%;
justify-content: space-between;
}
.items {
display: flex;
justify-content: center;
align-items: center;
@ -33,17 +55,77 @@
}
@media only screen and (max-width: 992px) {
.title {
justify-content: center;
.nav {
font-size: var(--font-size-large);
justify-content: space-between;
margin: 20px 0;
}
.items {
flex-wrap: wrap;
}
}
@media only screen and (max-width: 768px) {
.header {
padding: 0 15px;
}
.nav {
font-size: var(--font-size-large);
font-size: var(--font-size-normal);
flex-wrap: wrap;
justify-content: center;
padding: 20px 0;
flex-direction: column;
position: relative;
}
.buttons {
justify-content: center;
.items {
display: flex;
justify-content: unset;
align-items: left;
font-size: var(--font-size-normal);
font-weight: 600;
}
.items > div {
display: none;
}
.header .active {
display: inherit;
width: 100%;
}
.items a {
width: 100%;
}
.burger {
display: block;
/* color: #4a4a4a; */
cursor: pointer;
height: 3.25rem;
width: 3.25rem;
margin-left: auto;
position: absolute;
right: 0;
top: 0;
}
.burger span {
transform: translateX(-50%);
padding: 1px 0px;
margin: 6px 0;
width: 20px;
display: block;
background-color: white;
}
.burger div {
height: 100%;
color: white;
text-align: center;
margin: auto;
font-size: 1.5rem;
transform: translateX(-50%);
}
}

View File

@ -8,11 +8,6 @@ export default function Layout({ title, children, header = true, footer = true }
<>
<Head>
<title>umami{title && ` - ${title}`}</title>
<link rel="icon" href="/favicon.ico" />
<link
href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;600&display=swap"
rel="stylesheet"
/>
</Head>
{header && <Header />}
<main className="container">{children}</main>

View File

@ -3,7 +3,7 @@ import classNames from 'classnames';
import ChartJS from 'chart.js';
import Legend from 'components/metrics/Legend';
import { formatLongNumber } from 'lib/format';
import { dateFormat } from 'lib/lang';
import { dateFormat } from 'lib/date';
import useLocale from 'hooks/useLocale';
import useTheme from 'hooks/useTheme';
import { DEFAUL_CHART_HEIGHT, DEFAULT_ANIMATION_DURATION, THEME_COLORS } from 'lib/constants';
@ -40,26 +40,35 @@ export default function BarChart({
function renderXLabel(label, index, values) {
if (loading) return '';
const d = new Date(values[index].value);
const w = canvas.current.width;
const sw = canvas.current.width / window.devicePixelRatio;
switch (unit) {
case 'minute':
return index % 2 === 0 ? dateFormat(d, 'h:mm', locale) : '';
return index % 2 === 0 ? dateFormat(d, 'H:mm', locale) : '';
case 'hour':
return dateFormat(d, 'ha', locale);
return dateFormat(d, 'p', locale);
case 'day':
if (records > 31) {
if (w <= 500) {
if (records > 25) {
if (sw <= 275) {
return index % 10 === 0 ? dateFormat(d, 'M/d', locale) : '';
}
return index % 5 === 0 ? dateFormat(d, 'M/d', locale) : '';
if (sw <= 550) {
return index % 5 === 0 ? dateFormat(d, 'M/d', locale) : '';
}
if (sw <= 700) {
return index % 2 === 0 ? dateFormat(d, 'M/d', locale) : '';
}
return dateFormat(d, 'MMM d', locale);
}
if (w <= 500) {
if (sw <= 375) {
return index % 2 === 0 ? dateFormat(d, 'MMM d', locale) : '';
}
if (sw <= 425) {
return dateFormat(d, 'MMM d', locale);
}
return dateFormat(d, 'EEE M/d', locale);
case 'month':
if (w <= 660) {
if (sw <= 330) {
return index % 2 === 0 ? dateFormat(d, 'MMM', locale) : '';
}
return dateFormat(d, 'MMM', locale);
@ -93,9 +102,9 @@ export default function BarChart({
function getTooltipFormat(unit) {
switch (unit) {
case 'hour':
return 'EEE ha — MMM d yyyy';
return 'EEE p — PPP';
default:
return 'EEE MMMM d yyyy';
return 'PPPP';
}
}
@ -131,6 +140,7 @@ export default function BarChart({
minRotation: 0,
maxRotation: 0,
fontColor: colors.text,
autoSkipPadding: 1,
},
gridLines: {
display: false,
@ -175,6 +185,7 @@ export default function BarChart({
options.scales.xAxes[0].ticks.callback = renderXLabel;
options.scales.xAxes[0].ticks.fontColor = colors.text;
options.scales.yAxes[0].ticks.fontColor = colors.text;
options.scales.yAxes[0].ticks.precision = 0;
options.scales.yAxes[0].gridLines.color = colors.line;
options.scales.yAxes[0].gridLines.zeroLineColor = colors.zeroLine;
options.animation.duration = animationDuration;

View File

@ -10,7 +10,11 @@ export default function CountriesTable({ websiteId, onDataLoad, ...props }) {
const countryNames = useCountryNames(locale);
function renderLabel({ x }) {
return <div className={locale}>{countryNames[x]}</div>;
return (
<div className={locale}>
{countryNames[x] ?? <FormattedMessage id="label.unknown" defaultMessage="Unknown" />}
</div>
);
}
return (

View File

@ -2,6 +2,7 @@ import React, { useState } from 'react';
import { FixedSizeList } from 'react-window';
import { useSpring, animated, config } from 'react-spring';
import classNames from 'classnames';
import { FormattedMessage } from 'react-intl';
import NoData from 'components/common/NoData';
import { formatNumber, formatLongNumber } from 'lib/format';
import styles from './DataTable.module.css';
@ -27,7 +28,11 @@ export default function DataTable({
return (
<AnimatedRow
key={label}
label={renderLabel ? renderLabel(row) : label}
label={
renderLabel
? renderLabel(row)
: label ?? <FormattedMessage id="label.unknown" defaultMessage="Unknown" />
}
value={value}
percent={percent}
animate={animate && !virtualize}

View File

@ -1,13 +1,15 @@
.table {
position: relative;
height: 100%;
font-size: var(--font-size-small);
display: flex;
flex-direction: column;
flex: 1;
display: grid;
grid-template-rows: fit-content(100%) auto;
overflow: hidden;
}
.body {
position: relative;
height: 100%;
overflow: auto;
}

View File

@ -1,6 +1,5 @@
import React from 'react';
import MetricsTable from './MetricsTable';
import { deviceFilter } from 'lib/filters';
import { FormattedMessage } from 'react-intl';
import { getDeviceMessage } from 'components/messages';
@ -12,7 +11,6 @@ export default function DevicesTable({ websiteId, ...props }) {
type="device"
metric={<FormattedMessage id="metrics.visitors" defaultMessage="Visitors" />}
websiteId={websiteId}
dataFilter={deviceFilter}
renderLabel={({ x }) => getDeviceMessage(x)}
/>
);

View File

@ -1,23 +1,51 @@
import React from 'react';
import React, { useState } from 'react';
import { FormattedMessage } from 'react-intl';
import MetricsTable from './MetricsTable';
import Tag from 'components/common/Tag';
import DropDown from 'components/common/DropDown';
import { eventTypeFilter } from 'lib/filters';
import styles from './EventsTable.module.css';
const EVENT_FILTER_DEFAULT = {
value: 'EVENT_FILTER_DEFAULT',
label: <FormattedMessage id="label.all-events" defaultMessage="All events" />,
};
export default function EventsTable({ websiteId, ...props }) {
const [eventType, setEventType] = useState(EVENT_FILTER_DEFAULT.value);
const [eventTypes, setEventTypes] = useState([]);
const dropDownOptions = [EVENT_FILTER_DEFAULT, ...eventTypes.map(t => ({ value: t, label: t }))];
function handleDataLoad(data) {
setEventTypes([...new Set(data.map(({ x }) => x.split('\t')[0]))]);
props.onDataLoad?.(data);
}
return (
<MetricsTable
{...props}
title={<FormattedMessage id="metrics.events" defaultMessage="Events" />}
type="event"
metric={<FormattedMessage id="metrics.actions" defaultMessage="Actions" />}
websiteId={websiteId}
renderLabel={({ x }) => <Label value={x} />}
/>
<>
{eventTypes?.length > 1 && (
<div className={styles.filter}>
<DropDown value={eventType} options={dropDownOptions} onChange={setEventType} />
</div>
)}
<MetricsTable
{...props}
title={<FormattedMessage id="metrics.events" defaultMessage="Events" />}
type="event"
metric={<FormattedMessage id="metrics.actions" defaultMessage="Actions" />}
websiteId={websiteId}
dataFilter={eventTypeFilter}
filterOptions={eventType === EVENT_FILTER_DEFAULT.value ? [] : [eventType]}
renderLabel={({ x }) => <Label value={x} />}
onDataLoad={handleDataLoad}
/>
</>
);
}
const Label = ({ value }) => {
const [event, label] = value.split(':');
const [event, label] = value.split('\t');
return (
<>
<Tag>{event}</Tag>

View File

@ -0,0 +1,6 @@
.filter {
display: flex;
justify-content: flex-start;
align-items: center;
margin-bottom: 15px;
}

View File

@ -41,6 +41,7 @@ export default function MetricsBar({ websiteId, className }) {
}
const { pageviews, uniques, bounces, totaltime } = data || {};
const num = Math.min(uniques, bounces);
return (
<div className={classNames(styles.bar, className)} onClick={handleSetFormat}>
@ -60,7 +61,7 @@ export default function MetricsBar({ websiteId, className }) {
/>
<MetricCard
label={<FormattedMessage id="metrics.bounce-rate" defaultMessage="Bounce rate" />}
value={pageviews ? (bounces / pageviews) * 100 : 0}
value={uniques ? (num / uniques) * 100 : 0}
format={n => Number(n).toFixed(0) + '%'}
/>
<MetricCard

View File

@ -11,8 +11,6 @@
@media only screen and (max-width: 992px) {
.bar {
justify-content: space-between;
}
.bar > div:nth-child(n + 3) {
display: none;
overflow: auto;
}
}

View File

@ -17,7 +17,6 @@ import styles from './MetricsTable.module.css';
export default function MetricsTable({
websiteId,
websiteDomain,
type,
className,
dataFilter,
@ -42,7 +41,6 @@ export default function MetricsTable({
type,
start_at: +startDate,
end_at: +endDate,
domain: websiteDomain,
url,
},
onDataLoad,

View File

@ -1,6 +1,7 @@
.container {
position: relative;
min-height: 430px;
height: 100%;
font-size: var(--font-size-small);
display: flex;
flex-direction: column;

View File

@ -1,6 +1,5 @@
import React from 'react';
import MetricsTable from './MetricsTable';
import { osFilter } from 'lib/filters';
import { FormattedMessage } from 'react-intl';
export default function OSTable({ websiteId, ...props }) {
@ -11,7 +10,6 @@ export default function OSTable({ websiteId, ...props }) {
type="os"
metric={<FormattedMessage id="metrics.visitors" defaultMessage="Visitors" />}
websiteId={websiteId}
dataFilter={osFilter}
/>
);
}

View File

@ -2,11 +2,11 @@ import React, { useMemo, useState } from 'react';
import { FormattedMessage, useIntl } from 'react-intl';
import { FixedSizeList } from 'react-window';
import firstBy from 'thenby';
import { format } from 'date-fns';
import Icon from 'components/common/Icon';
import Tag from 'components/common/Tag';
import Dot from 'components/common/Dot';
import FilterButtons from 'components/common/FilterButtons';
import NoData from 'components/common/NoData';
import { devices } from 'components/messages';
import useLocale from 'hooks/useLocale';
import useCountryNames from 'hooks/useCountryNames';
@ -15,8 +15,8 @@ import Bolt from 'assets/bolt.svg';
import Visitor from 'assets/visitor.svg';
import Eye from 'assets/eye.svg';
import { stringToColor } from 'lib/format';
import { dateFormat } from 'lib/date';
import styles from './RealtimeLog.module.css';
import NoData from '../common/NoData';
const TYPE_ALL = 0;
const TYPE_PAGEVIEW = 1;
@ -29,7 +29,7 @@ const TYPE_ICONS = {
[TYPE_EVENT]: <Bolt />,
};
export default function RealtimeLog({ data, websites }) {
export default function RealtimeLog({ data, websites, websiteId }) {
const intl = useIntl();
const [locale] = useLocale();
const countryNames = useCountryNames(locale);
@ -88,7 +88,7 @@ export default function RealtimeLog({ data, websites }) {
}
function getWebsite({ website_id }) {
return websites.find(n => n.website_id === website_id)?.name;
return websites.find(n => n.website_id === website_id);
}
function getDetail({
@ -101,6 +101,7 @@ export default function RealtimeLog({ data, websites }) {
os,
country,
device,
website_id,
}) {
if (event_type) {
return (
@ -110,7 +111,17 @@ export default function RealtimeLog({ data, websites }) {
);
}
if (view_id) {
return url;
const domain = getWebsite({ website_id })?.domain;
return (
<a
className={styles.link}
href={`//${domain}${url}`}
target="_blank"
rel="noreferrer noopener"
>
{url}
</a>
);
}
if (session_id) {
return (
@ -118,7 +129,12 @@ export default function RealtimeLog({ data, websites }) {
id="message.log.visitor"
defaultMessage="Visitor from {country} using {browser} on {os} {device}"
values={{
country: <b>{countryNames[country]}</b>,
country: (
<b>
{countryNames[country] ||
intl.formatMessage({ id: 'label.unknown', defaultMessage: 'Unknown' })}
</b>
),
browser: <b>{BROWSERS[browser]}</b>,
os: <b>{os}</b>,
device: <b>{intl.formatMessage(devices[device])?.toLowerCase()}</b>,
@ -129,7 +145,7 @@ export default function RealtimeLog({ data, websites }) {
}
function getTime({ created_at }) {
return format(new Date(created_at), 'h:mm:ss');
return dateFormat(new Date(created_at), 'pp', locale);
}
function getColor(row) {
@ -150,7 +166,9 @@ export default function RealtimeLog({ data, websites }) {
<Icon className={styles.icon} icon={getIcon(row)} />
{getDetail(row)}
</div>
<div className={styles.website}>{getWebsite(row)}</div>
{!websiteId && websites.length > 1 && (
<div className={styles.website}>{getWebsite(row)?.domain}</div>
)}
</div>
);
};
@ -163,9 +181,11 @@ export default function RealtimeLog({ data, websites }) {
</div>
<div className={styles.body}>
{logs?.length === 0 && <NoData />}
<FixedSizeList height={400} itemCount={logs.length} itemSize={40}>
{Row}
</FixedSizeList>
{logs?.length > 0 && (
<FixedSizeList height={400} itemCount={logs.length} itemSize={40}>
{Row}
</FixedSizeList>
)}
</div>
</div>
);

View File

@ -1,6 +1,9 @@
.table {
font-size: var(--font-size-xsmall);
overflow: hidden;
height: 100%;
display: grid;
grid-template-rows: fit-content(100%) fit-content(100%) auto;
}
.header {
@ -21,6 +24,7 @@
.body {
overflow: auto;
height: 100%;
}
.icon {
@ -44,3 +48,12 @@
text-overflow: ellipsis;
overflow: hidden;
}
.row .link {
color: var(--gray900);
text-decoration: none;
}
.row .link:hover {
color: var(--primary400);
}

View File

@ -13,7 +13,10 @@ export default function RealtimeViews({ websiteId, data, websites }) {
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,
id =>
websites.length === 1
? websites[0]?.domain
: websites.find(({ website_id }) => website_id === id)?.domain,
[websites],
);
@ -28,6 +31,15 @@ export default function RealtimeViews({ websiteId, data, websites }) {
},
];
const renderLink = ({ x }) => {
const domain = x.startsWith('/') ? getDomain(websiteId) : '';
return (
<a href={`//${domain}${x}`} target="_blank" rel="noreferrer noopener">
{x}
</a>
);
};
const [referrers, pages] = useMemo(() => {
if (pageviews) {
const referrers = percentFilter(
@ -55,7 +67,7 @@ export default function RealtimeViews({ websiteId, data, websites }) {
pageviews
.reduce((arr, { url, website_id }) => {
if (url?.startsWith('/')) {
if (!websiteId) {
if (!websiteId && websites.length > 1) {
url = `${getDomain(website_id)}${url}`;
}
const row = arr.find(({ x }) => x === url);
@ -91,6 +103,7 @@ export default function RealtimeViews({ websiteId, data, websites }) {
<DataTable
title={<FormattedMessage id="metrics.pages" defaultMessage="Pages" />}
metric={<FormattedMessage id="metrics.views" defaultMessage="Views" />}
renderLabel={renderLink}
data={pages}
height={400}
/>

View File

@ -42,7 +42,6 @@ export default function ReferrersTable({ websiteId, websiteDomain, showFilters,
type="referrer"
metric={<FormattedMessage id="metrics.views" defaultMessage="Views" />}
websiteId={websiteId}
websiteDomain={websiteDomain}
dataFilter={refFilter}
filterOptions={{
domain: websiteDomain,

View File

@ -10,12 +10,23 @@ import Arrow from 'assets/arrow-right.svg';
import styles from './WebsiteHeader.module.css';
export default function WebsiteHeader({ websiteId, title, domain, showLink = false }) {
const header = showLink ? (
<>
<Favicon domain={domain} />
<Link href="/website/[...id]" as={`/website/${websiteId}/${title}`}>
{title}
</Link>
</>
) : (
<div>
<Favicon domain={domain} />
{title}
</div>
);
return (
<PageHeader>
<div className={styles.title}>
<Favicon domain={domain} />
{title}
</div>
<div className={styles.title}>{header}</div>
<ActiveUsers className={styles.active} websiteId={websiteId} />
<ButtonLayout align="right">
<RefreshButton websiteId={websiteId} />

View File

@ -1,5 +1,5 @@
.title {
color: var(--gray-900);
color: var(--gray900);
font-size: var(--font-size-large);
line-height: var(--font-size-large);
}

View File

@ -135,7 +135,7 @@ export default function RealtimeDashboard() {
<RealtimeViews websiteId={websiteId} data={realtimeData} websites={websites} />
</GridColumn>
<GridColumn xs={12} lg={8}>
<RealtimeLog data={realtimeData} websites={websites} />
<RealtimeLog websiteId={websiteId} data={realtimeData} websites={websites} />
</GridColumn>
</GridRow>
<GridRow>

View File

@ -43,19 +43,12 @@ export default function WebsiteDetails({ websiteId }) {
const [eventsData, setEventsData] = useState();
const {
resolve,
router,
query: { view },
} = usePageQuery();
const BackButton = () => (
<div key="back-button" className={styles.backButton}>
<Link
key="back-button"
href={router.pathname}
as={resolve({ view: undefined })}
icon={<Arrow />}
size="small"
>
<Link key="back-button" href={resolve({ view: undefined })} icon={<Arrow />} size="small">
<FormattedMessage id="label.back" defaultMessage="Back" />
</Link>
</div>

View File

@ -29,14 +29,13 @@ export default function AccountSettings() {
const Checkmark = ({ is_admin }) => (is_admin ? <Icon icon={<Check />} size="medium" /> : null);
const DashboardLink = row =>
row.is_admin ? null : (
<Link href={`/dashboard/${row.user_id}/${row.username}`}>
<a>
<Icon icon={<LinkIcon />} />
</a>
</Link>
);
const DashboardLink = row => (
<Link href={`/dashboard/${row.user_id}/${row.username}`}>
<a>
<Icon icon={<LinkIcon />} />
</a>
</Link>
);
const Buttons = row =>
row.username !== 'admin' ? (

View File

@ -6,13 +6,15 @@ import useDateRange from 'hooks/useDateRange';
import { DEFAULT_DATE_RANGE } from 'lib/constants';
import { getDateRange } from 'lib/date';
import styles from './DateRangeSetting.module.css';
import useLocale from 'hooks/useLocale';
export default function DateRangeSetting() {
const [locale] = useLocale();
const [dateRange, setDateRange] = useDateRange();
const { startDate, endDate, value } = dateRange;
function handleReset() {
setDateRange(getDateRange(DEFAULT_DATE_RANGE));
setDateRange(getDateRange(DEFAULT_DATE_RANGE, locale));
}
return (

View File

@ -24,7 +24,7 @@ export default function LanguageButton() {
)}
{locale === 'zh-TW' && (
<link
href="https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@400;500;700&display=swap"
href="https://fonts.googleapis.com/css2?family=Noto+Sans+TC:wght@400;500;700&display=swap"
rel="stylesheet"
/>
)}

View File

@ -1,7 +1,7 @@
.menu {
display: flex;
flex-flow: row wrap;
min-width: 500px;
min-width: 560px;
max-width: 100vw;
padding: 10px;
}

View File

@ -5,6 +5,7 @@
justify-content: center;
align-items: center;
cursor: pointer;
padding-bottom: 3px;
}
.button svg {

View File

@ -5,9 +5,11 @@ import { getItem, setItem } from 'lib/web';
import { setDateRange } from '../redux/actions/websites';
import { DATE_RANGE_CONFIG, DEFAULT_DATE_RANGE } from 'lib/constants';
import useForceUpdate from './useForceUpdate';
import useLocale from './useLocale';
export default function useDateRange(websiteId, defaultDateRange = DEFAULT_DATE_RANGE) {
const dispatch = useDispatch();
const [locale] = useLocale();
const dateRange = useSelector(state => state.websites[websiteId]?.dateRange);
const forceUpdate = useForceUpdate();
@ -16,7 +18,7 @@ export default function useDateRange(websiteId, defaultDateRange = DEFAULT_DATE_
if (globalDefault) {
if (typeof globalDefault === 'string') {
globalDateRange = getDateRange(globalDefault);
globalDateRange = getDateRange(globalDefault, locale);
} else if (typeof globalDefault === 'object') {
globalDateRange = {
...globalDefault,
@ -37,5 +39,5 @@ export default function useDateRange(websiteId, defaultDateRange = DEFAULT_DATE_
}
}
return [dateRange || globalDateRange || getDateRange(defaultDateRange), saveDateRange];
return [dateRange || globalDateRange || getDateRange(defaultDateRange, locale), saveDateRange];
}

11
hooks/useDelete.js Normal file
View File

@ -0,0 +1,11 @@
import { useCallback } from 'react';
import { useRouter } from 'next/router';
import { del } from 'lib/web';
export default function useDelete() {
const { basePath } = useRouter();
return useCallback(async (url, params, headers) => {
return del(`${basePath}${url}`, params, headers);
}, []);
}

View File

@ -6,8 +6,7 @@ import { useRouter } from 'next/router';
export default function useFetch(url, options = {}, update = []) {
const dispatch = useDispatch();
const [data, setData] = useState();
const [status, setStatus] = useState();
const [response, setResponse] = useState();
const [error, setError] = useState();
const [loading, setLoadiing] = useState(false);
const [count, setCount] = useState(0);
@ -19,18 +18,17 @@ export default function useFetch(url, options = {}, update = []) {
setLoadiing(true);
setError(null);
const time = performance.now();
const { data, status } = await get(`${basePath}${url}`, params, headers);
const { data, status, ok } = await get(`${basePath}${url}`, params, headers);
dispatch(updateQuery({ url, time: performance.now() - time, completed: Date.now() }));
if (status >= 400) {
setError(data);
setData(null);
setResponse({ data: null, status, ok });
} else {
setData(data);
setResponse({ data, status, ok });
}
setStatus(status);
onDataLoad?.(data);
} catch (e) {
console.error(e);
@ -60,5 +58,5 @@ export default function useFetch(url, options = {}, update = []) {
}
}, [interval, !!disabled]);
return { data, status, error, loading };
return { ...response, error, loading };
}

View File

@ -1,6 +1,6 @@
import { useMemo } from 'react';
import { useRouter } from 'next/router';
import { getQueryString } from '../lib/url';
import { getQueryString } from 'lib/url';
export default function usePageQuery() {
const router = useRouter();
@ -25,7 +25,9 @@ export default function usePageQuery() {
function resolve(params) {
const search = getQueryString({ ...query, ...params });
return `${pathname}${search}`;
const { asPath } = router;
return `${asPath.split('?')[0]}${search}`;
}
return { pathname, query, resolve, router };

11
hooks/usePost.js Normal file
View File

@ -0,0 +1,11 @@
import { useCallback } from 'react';
import { useRouter } from 'next/router';
import { post } from 'lib/web';
export default function usePost() {
const { basePath } = useRouter();
return useCallback(async (url, params, headers) => {
return post(`${basePath}${url}`, params, headers);
}, []);
}

View File

@ -2,17 +2,7 @@ import { useState, useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { updateUser } from 'redux/actions/user';
import { useRouter } from 'next/router';
import { get } from '../lib/web';
export async function fetchUser() {
const res = await fetch('/api/auth/verify');
if (!res.ok) {
return null;
}
return await res.json();
}
import { get } from 'lib/web';
export default function useRequireLogin() {
const router = useRouter();

View File

@ -1,14 +1,16 @@
import { useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { useRouter } from 'next/router';
import { get } from 'lib/web';
import { setShareToken } from 'redux/actions/app';
export default function useShareToken(shareId) {
const { basePath } = useRouter();
const dispatch = useDispatch();
const shareToken = useSelector(state => state.app.shareToken);
async function loadToken(id) {
const { data } = await get(`/api/share/${id}`);
const { data } = await get(`${basePath}/api/share/${id}`);
if (data) {
dispatch(setShareToken(data));

View File

@ -1,4 +1,5 @@
{
"cs-CZ": ["label.reset", "metrics.device.tablet"],
"de-DE": [
"label.administrator",
"label.name",

99
lang/cs-CZ.json Normal file
View File

@ -0,0 +1,99 @@
{
"label.accounts": "Účty",
"label.add-account": "Přidat účet",
"label.add-website": "Přidat web",
"label.administrator": "Administrátor",
"label.all": "Vše",
"label.all-websites": "Všechny weby",
"label.back": "Zpět",
"label.cancel": "Zrušit",
"label.change-password": "Změnit heslo",
"label.confirm-password": "Potvrdit heslo",
"label.copy-to-clipboard": "Kopírovat do schránky",
"label.current-password": "Aktuální heslo",
"label.custom-range": "Vlastní rozsah",
"label.dashboard": "Přehled",
"label.date-range": "Období",
"label.default-date-range": "Výchozí období",
"label.delete": "Smazat",
"label.delete-account": "Smazat účet",
"label.delete-website": "Smazat web",
"label.dismiss": "Odejít",
"label.domain": "Doména",
"label.edit": "Upravit",
"label.edit-account": "Upravit účet",
"label.edit-website": "Upravit web",
"label.enable-share-url": "Povolit sdílení URL",
"label.invalid": "Neplatný",
"label.invalid-domain": "Neplatná doména",
"label.last-days": "Posledních {x} dnů",
"label.last-hours": "Posledních {x} hodin",
"label.logged-in-as": "Přihlášený jako {username}",
"label.login": "Přihlásit",
"label.logout": "Odhlásit",
"label.more": "Více",
"label.name": "Jméno",
"label.new-password": "Nové heslo",
"label.password": "Heslo",
"label.passwords-dont-match": "Hesla se neschodují",
"label.profile": "Profil",
"label.realtime": "Aktuálně",
"label.realtime-logs": "Aktuální záznamy",
"label.refresh": "Obnovit",
"label.required": "Vyžadováno",
"label.reset": "Reset",
"label.save": "Uložit",
"label.settings": "Nastavení",
"label.share-url": "Sdílet URL",
"label.single-day": "Jeden den",
"label.this-month": "Tento měsíc",
"label.this-week": "Tento týden",
"label.this-year": "Tento rok",
"label.timezone": "Časová zóna",
"label.today": "Dnes",
"label.tracking-code": "Sledovací kód",
"label.unknown": "Neznámý",
"label.username": "Uživatelské jméno",
"label.view-details": "Zobrazit detaily",
"label.websites": "Weby",
"message.active-users": "{x} aktuálně {x, plural, one {návštěvník} other {návštěvníci}}",
"message.confirm-delete": "Opravdu smazat {target}?",
"message.copied": "Zkopírováno!",
"message.delete-warning": "Všechna související data budou také smazána.",
"message.failure": "Něco se pokazilo.",
"message.get-share-url": "Získat sdílené URL",
"message.get-tracking-code": "Získat měřící kód",
"message.go-to-settings": "Jít do nastavení",
"message.incorrect-username-password": "Nesprávné jméno/heslo.",
"message.log.visitor": "Návštěvník z {country} s prohlížečem {browser} na {os} {device}",
"message.new-version-available": "Nová verze umami {version} je k dispozici!",
"message.no-data-available": "Žádná data.",
"message.no-websites-configured": "Nemáte nastavený žádný web.",
"message.page-not-found": "Stránka nenalezena.",
"message.powered-by": "Běží na {name}",
"message.save-success": "Úspěšně uloženo.",
"message.share-url": "Toto je sdílené URL pro {target}.",
"message.track-stats": "Pro sledování návštěv na {target}, přidejte následující kód do {head} části vašeho webu.",
"message.type-delete": "Napište {delete} pro potvrzení.",
"metrics.actions": "Akce",
"metrics.average-visit-time": "Průměrný čas návštěvy",
"metrics.bounce-rate": "Okamžité opuštění",
"metrics.browsers": "Prohlížeč",
"metrics.countries": "Země",
"metrics.device.desktop": "Stolní počítač",
"metrics.device.laptop": "Přenosný počítač",
"metrics.device.mobile": "Mobilní telefon",
"metrics.device.tablet": "Tablet",
"metrics.devices": "Zařízení",
"metrics.events": "Události",
"metrics.filter.combined": "Kombinace",
"metrics.filter.domain-only": "Domény",
"metrics.filter.raw": "Nezpracované",
"metrics.operating-systems": "Operační systém",
"metrics.page-views": "Zobrazení stránek",
"metrics.pages": "Stránky",
"metrics.referrers": "Odkazy",
"metrics.unique-visitors": "Jedinečné návštěvy",
"metrics.views": "Zobrazení",
"metrics.visitors": "Návštěvy"
}

View File

@ -3,8 +3,8 @@
"label.add-account": "Tilføj konto",
"label.add-website": "Tilføj hjemmeside",
"label.administrator": "Administrator",
"label.all": "All",
"label.all-websites": "All websites",
"label.all": "Alle",
"label.all-websites": "Alle websites",
"label.back": "Tilbage",
"label.cancel": "Afvis",
"label.change-password": "Skift adgangskode",
@ -14,11 +14,11 @@
"label.custom-range": "Tilpasset interval",
"label.dashboard": "Betjeningspanel",
"label.date-range": "Datointerval",
"label.default-date-range": "Default date range",
"label.default-date-range": "Standard datointerval",
"label.delete": "Slet",
"label.delete-account": "Slet konto",
"label.delete-website": "Slet hjemmeside",
"label.dismiss": "Dismiss",
"label.dismiss": "Afvis",
"label.domain": "Domæne",
"label.edit": "Rediger",
"label.edit-account": "Rediger konto",
@ -37,8 +37,8 @@
"label.password": "Adgangskode",
"label.passwords-dont-match": "Adgangskoder matcher ikke",
"label.profile": "Profil",
"label.realtime": "Realtime",
"label.realtime-logs": "Realtime logs",
"label.realtime": "Realtid",
"label.realtime-logs": "Realtid logs",
"label.refresh": "Opdater",
"label.required": "Påkrævet",
"label.reset": "Reset",
@ -49,7 +49,7 @@
"label.this-month": "Denne måned",
"label.this-week": "Denne uge",
"label.this-year": "Dette år",
"label.timezone": "Timezone",
"label.timezone": "Tidszone",
"label.today": "Idag",
"label.tracking-code": "Sporingskode",
"label.unknown": "Ukendt",
@ -65,8 +65,8 @@
"message.get-tracking-code": "Få sporingskode",
"message.go-to-settings": "Gå til betjeningspanel",
"message.incorrect-username-password": "Ugyldigt brugernavn/adgangskode.",
"message.log.visitor": "Visitor from {country} using {browser} on {os} {device}",
"message.new-version-available": "A new version of umami {version} is available!",
"message.log.visitor": "Besøgende fra {country} bruger {browser} på {os} {device}",
"message.new-version-available": "Ny udgave af Umami {version} er tilgængelig!",
"message.no-data-available": "Ingen data tilgængelig.",
"message.no-websites-configured": "Du har ikke konfigureret nogen websteder.",
"message.page-not-found": "Side ikke fundet.",

View File

@ -5,6 +5,7 @@
"label.administrator": "Administrator",
"label.all": "Alle",
"label.all-websites": "Alle Webseiten",
"label.all-events": "Alle Ereignisse",
"label.back": "Zurück",
"label.cancel": "Abbrechen",
"label.change-password": "Passwort ändern",
@ -38,7 +39,7 @@
"label.passwords-dont-match": "Passwörter stimmen nicht überein",
"label.profile": "Profil",
"label.realtime": "Echtzeit",
"label.realtime-logs": "Echtzeit Logs",
"label.realtime-logs": "Echtzeit-Protokoll",
"label.refresh": "Aktualisieren",
"label.required": "Erforderlich",
"label.reset": "Zurücksetzen",
@ -57,23 +58,23 @@
"label.view-details": "Details anzeigen",
"label.websites": "Webseiten",
"message.active-users": "{x} {x, plural, one {aktiver Besucher} other {aktive Besucher}}",
"message.confirm-delete": "Sind sie sich sicher {target} zu löschen?",
"message.confirm-delete": "Sind Sie sich sicher {target} zu löschen?",
"message.copied": "In Zwischenablage kopiert!",
"message.delete-warning": "Alle zugehörigen Daten werden auch gelöscht.",
"message.failure": "Es it ein Fehler aufgetreten.",
"message.delete-warning": "Alle zugehörigen Daten werden ebenfalls gelöscht.",
"message.failure": "Es ist ein Fehler aufgetreten.",
"message.get-share-url": "Freigabe-URL abrufen",
"message.get-tracking-code": "Erstelle Tracking Kennung",
"message.go-to-settings": "Zu den Einstellungen",
"message.incorrect-username-password": "Falsches Passwort oder Benutzername.",
"message.log.visitor": "Besucher aus {country} benutzt {browser} auf {os} {device}",
"message.new-version-available": "Eine neue Version umami {version} ist verfügbar!",
"message.new-version-available": "Eine neue Version von umami {version} ist verfügbar!",
"message.no-data-available": "Keine Daten vorhanden.",
"message.no-websites-configured": "Es ist keine Webseite vorhanden.",
"message.page-not-found": "Seite nicht gefunden.",
"message.powered-by": "Ermöglicht durch {name}",
"message.powered-by": "Betrieben durch {name}",
"message.save-success": "Erfolgreich gespeichert.",
"message.share-url": "Dies ist der öffentliche URL zum Teilen für {target}.",
"message.track-stats": "Um die Statistiken für {target} zu übermitteln, platzieren Sie bitte den folgenden Quelltext im {head} ihrer Homepage.",
"message.share-url": "Dies ist die öffentliche URL zum Teilen für {target}.",
"message.track-stats": "Um die Statistiken für {target} zu übermitteln, platzieren Sie bitte den folgenden Quelltext im {head} ihrer Webseite.",
"message.type-delete": "Geben Sie {delete} in das Feld unten ein um zu bestätigen.",
"metrics.actions": "Aktionen",
"metrics.average-visit-time": "Durchschn. Besuchszeit",
@ -92,7 +93,7 @@
"metrics.operating-systems": "Betriebssysteme",
"metrics.page-views": "Seitenaufrufe",
"metrics.pages": "Seiten",
"metrics.referrers": "Referrers",
"metrics.referrers": "Referrer",
"metrics.unique-visitors": "Eindeutige Besucher",
"metrics.views": "Aufrufe",
"metrics.visitors": "Besucher"

View File

@ -5,6 +5,7 @@
"label.administrator": "Administrator",
"label.all": "All",
"label.all-websites": "All websites",
"label.all-events": "All events",
"label.back": "Back",
"label.cancel": "Cancel",
"label.change-password": "Change password",

View File

@ -5,6 +5,7 @@
"label.administrator": "Administrador",
"label.all": "Todos",
"label.all-websites": "Todos los sitios",
"label.all-events": "Todos los eventos",
"label.back": "Atrás",
"label.cancel": "Cancelar",
"label.change-password": "Cambiar contraseña",

100
lang/fa-IR.json Normal file
View File

@ -0,0 +1,100 @@
{
"label.accounts": "حساب ها",
"label.add-account": "افزودن حساب",
"label.add-website": "افزودن وب سایت",
"label.administrator": "مدیر",
"label.all": "همه",
"label.all-websites": "همه وب سایت ها",
"label.all-events": "همه رویداد ها",
"label.back": "برگشت",
"label.cancel": "انصراف",
"label.change-password": "تغییر رمز",
"label.confirm-password": "تایید رمز",
"label.copy-to-clipboard": "کپی به حافظه",
"label.current-password": "رمز فعلی",
"label.custom-range": "محدوده دلخواه",
"label.dashboard": "داشبورد",
"label.date-range": "محدوده تاریخ",
"label.default-date-range": "محدوده پیشفرض تاریخ",
"label.delete": "حذف",
"label.delete-account": "حذف حساب",
"label.delete-website": "حذف وب سایت",
"label.dismiss": "رد کردن",
"label.domain": "دامنه",
"label.edit": "ویرایش",
"label.edit-account": "ویرایش حساب",
"label.edit-website": "ویرایش وب سایت",
"label.enable-share-url": "فعال کردن اشتراک گذاری URL",
"label.invalid": "نامعتبر",
"label.invalid-domain": "دامنه نامعتبر",
"label.last-days": "لیست {x} روز",
"label.last-hours": "لیست {x} ساعت",
"label.logged-in-as": "وارد شده به عنوان {username}",
"label.login": "ورود",
"label.logout": "خروج",
"label.more": "بیشتر",
"label.name": "نام",
"label.new-password": "رمز جدید",
"label.password": "رمز",
"label.passwords-dont-match": "رمز ها یکسان نیستند",
"label.profile": "پروفایل",
"label.realtime": "آمار هم اکنون",
"label.realtime-logs": "لاگ های هم اکنون",
"label.refresh": "تازه کردن",
"label.required": "لازم",
"label.reset": "ریست",
"label.save": "ذخیره",
"label.settings": "تنظیمات",
"label.share-url": "به اشتراک گذاری URL",
"label.single-day": "یک روز",
"label.this-month": "این ماه",
"label.this-week": "این هفته",
"label.this-year": "امسال",
"label.timezone": "منطقه زمانی",
"label.today": "امروز",
"label.tracking-code": "کد رهگیری",
"label.unknown": "ناشناخته",
"label.username": "نام کاربری",
"label.view-details": "مشاهده جزئیات",
"label.websites": "وب سایت ها",
"message.active-users": "{x} هم اکنون {x, plural, one {یک} other {از میان}}",
"message.confirm-delete": "آیا مطمئن هستید می خواهید {target} را حذف کنید?",
"message.copied": "کپی شد!",
"message.delete-warning": "همه داده های مرتبط هم حذف خواهد شد.",
"message.failure": "مشکلی پیش آمده است.",
"message.get-share-url": "دریافت URL برای اشتراک گذاری",
"message.get-tracking-code": "گرفتن کد رهگیری",
"message.go-to-settings": "رفتن به تنظیمات",
"message.incorrect-username-password": "نام کاربری / رمز نادرست است.",
"message.log.visitor": "بازدید کننده از کشور {country} با مروگر {browser} در {os} {device}",
"message.new-version-available": "نسخه جدید umami ({version}) وجود است!",
"message.no-data-available": "اطلاعاتی موجود نیست.",
"message.no-websites-configured": "شما هیچ وب سایتی را پیکر بندی نکرده اید.",
"message.page-not-found": "صفحه یافت نشد.",
"message.powered-by": "قدرت گرفته توسط {name}",
"message.save-success": "با موفقیت ذخیره شد.",
"message.share-url": "این URL به اشتراک گذاشته شده عمومی برای {target} است.",
"message.track-stats": "برای ردیابی آمار {target}, کد روبرو را در قسمت {head} وب سایت قرار دهید.",
"message.type-delete": "جهت اطمینان '{delete}' را در کادر زیر بنویسید.",
"metrics.actions": "اقدامات",
"metrics.average-visit-time": "میانگین زمان بازدید",
"metrics.bounce-rate": "نرخ Bounce",
"metrics.browsers": "مروگر ها",
"metrics.countries": "کشور ها",
"metrics.device.desktop": "دسکتاپ",
"metrics.device.laptop": "لپ تاپ",
"metrics.device.mobile": "موبایل",
"metrics.device.tablet": "تبلت",
"metrics.devices": "دستگاه ها",
"metrics.events": "رویداد ها",
"metrics.filter.combined": "ترکیب شده",
"metrics.filter.domain-only": "فقط دامنه",
"metrics.filter.raw": "خام",
"metrics.operating-systems": "سیستم عامل ها",
"metrics.page-views": "بازدید صفحه",
"metrics.pages": "صفحه ها",
"metrics.referrers": "ارجاع دهندگان",
"metrics.unique-visitors": "بازدید کننده خالص",
"metrics.views": "بازدید",
"metrics.visitors": "بازدید کننده"
}

View File

@ -3,8 +3,8 @@
"label.add-account": "Lisää tili",
"label.add-website": "Lisää verkkosivu",
"label.administrator": "Järjestelmänvalvoja",
"label.all": "All",
"label.all-websites": "All websites",
"label.all": "Kaikki",
"label.all-websites": "Kaikki verkkosivut",
"label.back": "Takaisin",
"label.cancel": "Peruuta",
"label.change-password": "Vaihda salasana",
@ -12,7 +12,7 @@
"label.copy-to-clipboard": "Kopioi leikepöydälle",
"label.current-password": "Nykyinen salasana",
"label.custom-range": "Mukautettu jakso",
"label.dashboard": "Dashboard",
"label.dashboard": "Ohjauspaneeli",
"label.date-range": "Ajanjakso",
"label.default-date-range": "Oletusajanjakso",
"label.delete": "Poista",
@ -37,8 +37,8 @@
"label.password": "Salasana",
"label.passwords-dont-match": "Salasanat eivät täsmää",
"label.profile": "Profiili",
"label.realtime": "Realtime",
"label.realtime-logs": "Realtime logs",
"label.realtime": "Reaaliaikainen",
"label.realtime-logs": "Reaaliaikaiset lokit",
"label.refresh": "Päivitä",
"label.required": "Vaaditaan",
"label.reset": "Nollaa",
@ -65,7 +65,7 @@
"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.log.visitor": "Vierailija maasta {country} käyttäen selainta {browser} {os}-laitteella: {device}",
"message.new-version-available": "Uusi versio umamista {version} on käytettävissä!",
"message.no-data-available": "Tietoja ei ole käytettävissä.",
"message.no-websites-configured": "Sinulla ei ole määritettyjä verkkosivustoja.",

View File

@ -5,6 +5,7 @@
"label.administrator": "Administrateur",
"label.all": "Tout",
"label.all-websites": "Tous les sites web",
"label.all-events": "Tous les événements",
"label.back": "Retour",
"label.cancel": "Annuler",
"label.change-password": "Changer le mot de passe",

99
lang/he-IL.json Normal file
View File

@ -0,0 +1,99 @@
{
"label.accounts": "חשבונות",
"label.add-account": "הוספה",
"label.add-website": "הוספת אתר",
"label.administrator": "מנהל",
"label.all": "הכל",
"label.all-websites": "כל האתרים",
"label.back": "חזרה",
"label.cancel": "ביטול",
"label.change-password": "שינוי סיסמה",
"label.confirm-password": "אישור סיסמה",
"label.copy-to-clipboard": "העתקה",
"label.current-password": "סיסמה נוכחית",
"label.custom-range": "טווח מותאם",
"label.dashboard": "דשבורד",
"label.date-range": "טווח תאריכים",
"label.default-date-range": "טווח תאריכים בברירת מחדל",
"label.delete": "הסרה",
"label.delete-account": "הסרת חשבון",
"label.delete-website": "הסרת אתר",
"label.dismiss": "שיחרור",
"label.domain": "דומיין",
"label.edit": "עריכה",
"label.edit-account": "עריכת חשבון",
"label.edit-website": "עריכת אתר",
"label.enable-share-url": "הפעלת URL שיתוף",
"label.invalid": "לא תקין",
"label.invalid-domain": "דומיין לא תקין",
"label.last-days": "{x} ימים אחרונים",
"label.last-hours": "{x} שעות אחרונות",
"label.logged-in-as": "מחובר כ-{username}",
"label.login": "התחברות",
"label.logout": "התנתקות",
"label.more": "עוד",
"label.name": "שם",
"label.new-password": "סיסמה חדשה",
"label.password": "סיסמה",
"label.passwords-dont-match": "סיסמאות לא תואמות",
"label.profile": "פרופיל",
"label.realtime": "זמן אמת",
"label.realtime-logs": "לוגים - זמן אמת",
"label.refresh": "רענון",
"label.required": "נדרש",
"label.reset": "איפוס",
"label.save": "שמירה",
"label.settings": "הגדרות",
"label.share-url": "שיתוף URL",
"label.single-day": "יום בודד",
"label.this-month": "החודש",
"label.this-week": "השבוע",
"label.this-year": "השנה",
"label.timezone": "אזור זמן",
"label.today": "היום",
"label.tracking-code": "קוד מעקב",
"label.unknown": "לא ידוע",
"label.username": "שם משתמש",
"label.view-details": "פרטים נוספים",
"label.websites": "אתרים",
"message.active-users": "{x} נוכחיים {x, plural, one {מבקר} other {מבקרים}}",
"message.confirm-delete": "האם באמת למחוק את {target}?",
"message.copied": "הועתק!",
"message.delete-warning": "כל המידע המקושר יימחק",
"message.failure": "משהו השתבש",
"message.get-share-url": "קבלת URL שיתוף",
"message.get-tracking-code": "קבלת קוד מעקב",
"message.go-to-settings": "להדרותג",
"message.incorrect-username-password": "שם משתמש או סיסמה לא נכונים",
"message.log.visitor": "מבקר ממדינת {country} משתמבש בדפדפן {browser} ב-{os} {device}",
"message.new-version-available": "גרסה חדשה של Umami {version} זמינה!",
"message.no-data-available": "אין מידע זמין",
"message.no-websites-configured": "לא מוגדרים אתרים",
"message.page-not-found": "דף לא נמצא",
"message.powered-by": "Powered by {name}",
"message.save-success": "נשמר בהצלחה",
"message.share-url": "זהו URL ציבורי עבור {target}",
"message.track-stats": "יש להוסיף את הקוד הבא לאזור ה-{head} של האתר",
"message.type-delete": "הקלידו {delete} בתיבה על מנת לאשר",
"metrics.actions": "פעולות",
"metrics.average-visit-time": "זמן ביקור ממוצע",
"metrics.bounce-rate": "Bounce rate",
"metrics.browsers": "דפדפנים",
"metrics.countries": "מדינות",
"metrics.device.desktop": "דסקטופ",
"metrics.device.laptop": "לפטופ",
"metrics.device.mobile": "מובייל",
"metrics.device.tablet": "טאבלט",
"metrics.devices": "מכשירים",
"metrics.events": "אירועים",
"metrics.filter.combined": "משותף",
"metrics.filter.domain-only": "דומיין בלבד",
"metrics.filter.raw": "גולמי",
"metrics.operating-systems": "מערכות הפעלה",
"metrics.page-views": "צפיות בדפים",
"metrics.pages": "דפים",
"metrics.referrers": "מפנים",
"metrics.unique-visitors": "מבקרים ייחודיים",
"metrics.views": "צפיות",
"metrics.visitors": "מבקרים"
}

99
lang/hi-IN.json Normal file
View File

@ -0,0 +1,99 @@
{
"label.accounts": "खाता",
"label.add-account": "खाता जोड़ें",
"label.add-website": "वेबसाइट",
"label.administrator": "प्रशासक",
"label.all": "सब",
"label.all-websites": "सभी वेबसाइटें",
"label.back": "पीछे",
"label.cancel": "रद्द करें",
"label.change-password": "पासवर्ड बदलें",
"label.confirm-password": "पासवर्ड की पुष्टि कीजिये",
"label.copy-to-clipboard": "क्लिपबोर्ड पर कॉपी करें",
"label.current-password": "वर्तमान पासवर्ड",
"label.custom-range": "कस्टम रेंज",
"label.dashboard": "नियंत्रण-पट्ट",
"label.date-range": "तिथि सीमा",
"label.default-date-range": "डिफ़ॉल्ट तिथि सीमा",
"label.delete": "खाता हटाएं",
"label.delete-account": "खाता हटाएं",
"label.delete-website": "वेबसाइट हटाएं",
"label.dismiss": "खारिज कीजिये",
"label.domain": "डोमेन",
"label.edit": "संपादित करें",
"label.edit-account": "खाता संपादित करें",
"label.edit-website": "वेबसाइट संपादित करें",
"label.enable-share-url": "शेयर URL सक्षम करें",
"label.invalid": "अमान्य",
"label.invalid-domain": "अमान्य डोमेन",
"label.last-days": "पिछले {x} दिन",
"label.last-hours": "पिछले {x} घंटे",
"label.logged-in-as": "{x} के रूप में लॉग इन किया",
"label.login": "लॉग इन",
"label.logout": "लॉग आउट",
"label.more": "और",
"label.name": "नाम",
"label.new-password": "नया पासवर्ड",
"label.password": "पासवर्ड",
"label.passwords-dont-match": "पासवर्ड मेल नहीं खाते",
"label.profile": "प्रोफ़ाइल",
"label.realtime": "वास्तव काल",
"label.realtime-logs": "वास्तविक समय लॉग",
"label.refresh": "रिफ्रेश",
"label.required": "अपेक्षित",
"label.reset": "रीसेट",
"label.save": "सहेजें",
"label.settings": "समायोजन",
"label.share-url": "यूआरएल साझा करें",
"label.single-day": "एक दिन",
"label.this-month": "इस महीने",
"label.this-week": "इस सप्ताह",
"label.this-year": "इस साल",
"label.timezone": "समय क्षेत्र",
"label.today": "आज",
"label.tracking-code": "ट्रैकिंग कोड",
"label.unknown": "अज्ञात",
"label.username": "उपयोगकर्ता नाम",
"label.view-details": "विवरण देखें",
"label.websites": "वेबसाइटों",
"message.active-users": "{x} मौजूद {x, plural, one {आगंतुक} other {आगंतुकों}}",
"message.confirm-delete": "क्या आप वाकई में {target} हटाना चाहते हैं?",
"message.copied": "कॉपी हो गया!",
"message.delete-warning": "सभी संबद्ध डेटा को भी हटा दिया जाएगा।",
"message.failure": "कुछ गलत हो गया।",
"message.get-share-url": "शेयर URL प्राप्त करें",
"message.get-tracking-code": "ट्रैकिंग कोड प्राप्त करें",
"message.go-to-settings": "समायोजन में जाइए",
"message.incorrect-username-password": "ग़लत उपयोगकर्ता नाम / पासवर्ड।",
"message.log.visitor": "{country} का आगंतुक, जो {browser} का उपयोग करता है, {os} यन्त्र पर",
"message.new-version-available": "उमामी का नया संस्करण {version} उपलब्ध है!",
"message.no-data-available": "कोई डेटा उपलब्ध नहीं है।",
"message.no-websites-configured": "आपके पास कोई वेबसाइट कॉन्फ़िगर नहीं है।",
"message.page-not-found": "पृष्ठ नहीं मिला।",
"message.powered-by": "{name} द्वारा संचालित",
"message.save-success": "सफलतापूर्वक संचित कर लिया गया है।",
"message.share-url": "यह {target} के लिए सार्वजनिक रूप से साझा किया गया URL है।",
"message.track-stats": "{target} के आँकड़ों को ट्रैक करने के लिए, अपनी वेबसाइट के {head} अनुभाग में निम्नलिखित कोड रखें।",
"message.type-delete": "पुष्टि करने के लिए नीचे दिए गए बॉक्स में {delete} टाइप करें।",
"metrics.actions": "कार्य",
"metrics.average-visit-time": "औसत दृश्य समय",
"metrics.bounce-rate": "उछाल दर",
"metrics.browsers": "वेब ब्राउज़र",
"metrics.countries": "देश",
"metrics.device.desktop": "डेस्कटॉप",
"metrics.device.laptop": "लैपटॉप",
"metrics.device.mobile": "मोबाइल फोन",
"metrics.device.tablet": "टैबलेट",
"metrics.devices": "उपकरण",
"metrics.events": "स्पर्धाएँ",
"metrics.filter.combined": "संयुक्त",
"metrics.filter.domain-only": "केवल डोमेन",
"metrics.filter.raw": "रॉ",
"metrics.operating-systems": "ऑपरेटिंग सिस्टम",
"metrics.page-views": "पृष्ठ दृश्य",
"metrics.pages": "पृष्ठों",
"metrics.referrers": "सन्दर्भदाता",
"metrics.unique-visitors": "अद्वितीय आगंतुकों",
"metrics.views": "दृश्य",
"metrics.visitors": "आगंतुकों"
}

99
lang/it-IT.json Normal file
View File

@ -0,0 +1,99 @@
{
"label.accounts": "Utenti",
"label.add-account": "Aggiungi utente",
"label.add-website": "Aggiungi sito",
"label.administrator": "Amministratore",
"label.all": "Tutto",
"label.all-websites": "Tutti i siti web",
"label.back": "Indietro",
"label.cancel": "Annulla",
"label.change-password": "Modifica password",
"label.confirm-password": "Conferma password",
"label.copy-to-clipboard": "Copia",
"label.current-password": "Password corrente",
"label.custom-range": "Personalizzato",
"label.dashboard": "Dashboard",
"label.date-range": "Periodo",
"label.default-date-range": "Periodo standard",
"label.delete": "Elimina",
"label.delete-account": "Elimina account",
"label.delete-website": "Elimina sito",
"label.dismiss": "Scarta",
"label.domain": "Dominio",
"label.edit": "Modifica",
"label.edit-account": "Modifica account",
"label.edit-website": "Modifica sito",
"label.enable-share-url": "Abilita URL di condivisione",
"label.invalid": "Non valido",
"label.invalid-domain": "Dominio non valido",
"label.last-days": "Ultimi {x} giorni",
"label.last-hours": "Ultime {x} ore",
"label.logged-in-as": "Ciao {username}",
"label.login": "Login",
"label.logout": "Logout",
"label.more": "Dettagli",
"label.name": "Nome",
"label.new-password": "Nuova password",
"label.password": "Password",
"label.passwords-dont-match": "Le password non corrispondono",
"label.profile": "Profilo",
"label.realtime": "Realtime",
"label.realtime-logs": "Log in realtime",
"label.refresh": "Ricarica",
"label.required": "Obbligatorio",
"label.reset": "Reset",
"label.save": "Salva",
"label.settings": "Impostazioni",
"label.share-url": "Share URL",
"label.single-day": "Singolo giorno",
"label.this-month": "Questo mese",
"label.this-week": "Questa settimana",
"label.this-year": "Quest'anno",
"label.timezone": "Fuso orario",
"label.today": "Oggi",
"label.tracking-code": "Codice di tracking",
"label.unknown": "Sconosciuto",
"label.username": "Username",
"label.view-details": "Vedi dettagli",
"label.websites": "Siti web",
"message.active-users": "{x} {x, plural, one {visitatore} other {visitatori}} online",
"message.confirm-delete": "Sei sicuro di voler eliminare {target}?",
"message.copied": "Copiato!",
"message.delete-warning": "Saranno eliminati anche tutti i dati associati.",
"message.failure": "Si è verificato un errore.",
"message.get-share-url": "Ottieni l'URL di condivisione",
"message.get-tracking-code": "Ottieni il codice di tracking",
"message.go-to-settings": "Vai alle impostazioni",
"message.incorrect-username-password": "Username o password non corretti.",
"message.log.visitor": "Utenti da {country} tramite {browser} su {os} {device}",
"message.new-version-available": "Una nuova versione umami {version} è disponibile!",
"message.no-data-available": "Nessun dato disponibile.",
"message.no-websites-configured": "Non hai ancora configurato alcun sito.",
"message.page-not-found": "Pagina non trovata",
"message.powered-by": "Powered by {name}",
"message.save-success": "Salvato!",
"message.share-url": "Questo è l'URL di condivisione per {target}.",
"message.track-stats": "Per tracciare le statistiche di {target}, inserisci questo codice nella sezione {head} del tuo sito web.",
"message.type-delete": "Digita {delete} nel box qui sotto per confermare.",
"metrics.actions": "Azioni",
"metrics.average-visit-time": "Tempo medio di visita",
"metrics.bounce-rate": "Frequenza di rimbalzo",
"metrics.browsers": "Browser",
"metrics.countries": "Nazioni",
"metrics.device.desktop": "Desktop",
"metrics.device.laptop": "Laptop",
"metrics.device.mobile": "Mobile",
"metrics.device.tablet": "Tablet",
"metrics.devices": "Dispositivi",
"metrics.events": "Eventi",
"metrics.filter.combined": "Aggregati",
"metrics.filter.domain-only": "Solo dominio",
"metrics.filter.raw": "Raw",
"metrics.operating-systems": "Sistemi operativi",
"metrics.page-views": "Visualizzazioni di pagina",
"metrics.pages": "Pagine",
"metrics.referrers": "Referr",
"metrics.unique-visitors": "Visitatori unici",
"metrics.views": "Visualizzazioni",
"metrics.visitors": "Visitatori"
}

View File

@ -3,8 +3,8 @@
"label.add-account": "アカウントの追加",
"label.add-website": "Webサイトの追加",
"label.administrator": "管理者",
"label.all": "All",
"label.all-websites": "All websites",
"label.all": "すべて表示",
"label.all-websites": "すべてのWebサイト",
"label.back": "戻る",
"label.cancel": "キャンセル",
"label.change-password": "パスワード変更",
@ -13,7 +13,7 @@
"label.current-password": "現在のパスワード",
"label.custom-range": "期間を指定する",
"label.dashboard": "ダッシュボード",
"label.date-range": "日付範囲",
"label.date-range": "範囲指定",
"label.default-date-range": "最初に表示する期間",
"label.delete": "削除",
"label.delete-account": "アカウントの削除",
@ -37,8 +37,8 @@
"label.password": "パスワード",
"label.passwords-dont-match": "パスワードが一致しません",
"label.profile": "プロファイル",
"label.realtime": "Realtime",
"label.realtime-logs": "Realtime logs",
"label.realtime": "リアルタイム",
"label.realtime-logs": "リアルタイムログ",
"label.refresh": "更新",
"label.required": "必須",
"label.reset": "リセット",
@ -65,12 +65,12 @@
"message.get-tracking-code": "トラッキングコードを取得",
"message.go-to-settings": "設定する",
"message.incorrect-username-password": "ユーザー名/パスワードが正しくありません。",
"message.log.visitor": "Visitor from {country} using {browser} on {os} {device}",
"message.log.visitor": "{os}{device})で{browser}を使用している{country}からの訪問者",
"message.new-version-available": "新しいバージョン({version})が利用可能です!",
"message.no-data-available": "データがありません。",
"message.no-websites-configured": "Webサイトが設定されていません。",
"message.page-not-found": "ページが見つかりません。",
"message.powered-by": "Powered by {name}",
"message.powered-by": "このシステムは {name} で実行されています。",
"message.save-success": "正常に保存されました。",
"message.share-url": "これは {target} の共有リンクです。",
"message.track-stats": "{target}のアクセス解析を開始するには、次のコードをWebサイトの{head}セクションへ追加してください。",

View File

@ -93,7 +93,7 @@
"metrics.page-views": "Хуудас үзсэн",
"metrics.pages": "Хуудас",
"metrics.referrers": "Чиглүүлэгч",
"metrics.unique-visitors": "Зочид",
"metrics.unique-visitors": "Зочин",
"metrics.views": "Үзсэн",
"metrics.visitors": "Зочид"
"metrics.visitors": "Зочин"
}

100
lang/ms-MY.json Normal file
View File

@ -0,0 +1,100 @@
{
"label.accounts": "Akaun",
"label.add-account": "Tambah akaun",
"label.add-website": "Tambah laman web",
"label.administrator": "Pentadbir",
"label.all": "Semua",
"label.all-websites": "Semua laman web",
"label.all-events": "Semua peristiwa",
"label.back": "Kembali",
"label.cancel": "Batal",
"label.change-password": "Tukar kata laluan",
"label.confirm-password": "Sahkan kata laluan",
"label.copy-to-clipboard": "Salin ke papan keratan",
"label.current-password": "Kata laluan semasa",
"label.custom-range": "Julat khas",
"label.dashboard": "Papan pemuka",
"label.date-range": "Julat tarikh",
"label.default-date-range": "Julat tarikh lalai",
"label.delete": "Padam",
"label.delete-account": "Padam akaun",
"label.delete-website": "Padam laman web",
"label.dismiss": "Ketepikan",
"label.domain": "Domain",
"label.edit": "Edit",
"label.edit-account": "Edit akaun",
"label.edit-website": "Edit laman web",
"label.enable-share-url": "Aktifkan url berkongsi",
"label.invalid": "Tidak sah",
"label.invalid-domain": "Domain tidak sah",
"label.last-days": "{x} hari lepas",
"label.last-hours": "{x} jam lepas",
"label.logged-in-as": "Log masuk sebagai {username}",
"label.login": "Log masuk",
"label.logout": "Log keluar",
"label.more": "Lebih banyak lagi",
"label.name": "Nama",
"label.new-password": "Kata laluan baru",
"label.password": "Kata laluan",
"label.passwords-dont-match": "Kata laluan tidak sepadan",
"label.profile": "Profil",
"label.realtime": "Siaran langsung",
"label.realtime-logs": "Log secara siaran langsung",
"label.refresh": "Muat semula",
"label.required": "Diperlukan",
"label.reset": "Tetapkan semula",
"label.save": "Simpan",
"label.settings": "Tetapan",
"label.share-url": "Kongsikan URL",
"label.single-day": "Satu hari",
"label.this-month": "Bulan ini",
"label.this-week": "Minggu ini",
"label.this-year": "Tahun ini",
"label.timezone": "Zon masa",
"label.today": "Hari ini",
"label.tracking-code": "Kod penjejakan",
"label.unknown": "Tidak diketahui",
"label.username": "Nama pengguna",
"label.view-details": "Lihat butiran",
"label.websites": "Laman web",
"message.active-users": "{x} semasa {x, plural, one {pelawat} other {pelawat}}",
"message.confirm-delete": "Pastikah anda ingin memadam {target}?",
"message.copied": "Disalin!",
"message.delete-warning": "Semua data yang berkaitan juga akan dihapuskan.",
"message.failure": "Ada yang tidak kena.",
"message.get-share-url": "Dapatkan URL berkongsi",
"message.get-tracking-code": "Dapatkan kod penjejakan",
"message.go-to-settings": "Pergi ke tetapan",
"message.incorrect-username-password": "Pengguna/kata laluan tidak betul.",
"message.log.visitor": "Pelawat dari {country} mengguna {browser} pada {os} {device}",
"message.new-version-available": "Versi baru umami {version} boleh didapati!",
"message.no-data-available": "Tiada data yang boleh didapati.",
"message.no-websites-configured": "Anda tidak ada sebarang laman web yang telah dikonfigurasikan.",
"message.page-not-found": "Halaman tidak dijumpai.",
"message.powered-by": "Disediakan oleh {name}",
"message.save-success": "Berjaya disimpan.",
"message.share-url": "Ini adalah URL berkongsi untuk {target}.",
"message.track-stats": "Untuk menjejak statistik bagi {target}, letakkan kod berikut di bahagian {head} laman web anda.",
"message.type-delete": "Taip {delete} di dalam kotak di bawah untuk pengesahan.",
"metrics.actions": "Aksi",
"metrics.average-visit-time": "Purata tempoh masa lawatan",
"metrics.bounce-rate": "Kadar lantunan",
"metrics.browsers": "Pelayar web",
"metrics.countries": "Negara",
"metrics.device.desktop": "Desktop",
"metrics.device.laptop": "Laptop",
"metrics.device.mobile": "Telefon bimbit",
"metrics.device.tablet": "Tablet",
"metrics.devices": "Peranti",
"metrics.events": "Peristiwa",
"metrics.filter.combined": "Digabungkan",
"metrics.filter.domain-only": "Domain sahaja",
"metrics.filter.raw": "Mentah",
"metrics.operating-systems": "Sistem operasi",
"metrics.page-views": "Paparan halaman",
"metrics.pages": "Halaman",
"metrics.referrers": "Perujuk",
"metrics.unique-visitors": "Pelawat unik",
"metrics.views": "Lawatan",
"metrics.visitors": "Pelawat"
}

99
lang/pl-PL.json Normal file
View File

@ -0,0 +1,99 @@
{
"label.accounts": "Konta",
"label.add-account": "Dodaj konto",
"label.add-website": "Dodaj witrynę",
"label.administrator": "Administrator",
"label.all": "Wszystkie",
"label.all-websites": "Wszystkie witryny",
"label.back": "Powrót",
"label.cancel": "Anuluj",
"label.change-password": "Zmień hasło",
"label.confirm-password": "Potwierdź hasło",
"label.copy-to-clipboard": "Skopiuj do schowka",
"label.current-password": "Aktualne hasło",
"label.custom-range": "Zakres niestandardowy",
"label.dashboard": "Dashboard",
"label.date-range": "Zakres dat",
"label.default-date-range": "Domyślny zakres dat",
"label.delete": "Usuń",
"label.delete-account": "Usuń konto",
"label.delete-website": "Usuń witrynę",
"label.dismiss": "Odrzuć",
"label.domain": "Domena",
"label.edit": "Edytuj",
"label.edit-account": "Edytuj konto",
"label.edit-website": "Edytuj witrynę",
"label.enable-share-url": "Włącz udostępnianie adresu URL",
"label.invalid": "Nieprawidłowy",
"label.invalid-domain": "Nieprawidłowa witryna",
"label.last-days": "Ostatnie {x} dni",
"label.last-hours": "Ostatnie {x} godzin",
"label.logged-in-as": "Zalogowano jako {username}",
"label.login": "Zaloguj się",
"label.logout": "Wyloguj",
"label.more": "Więcej",
"label.name": "Nazwa",
"label.new-password": "Nowe hasło",
"label.password": "Hasło",
"label.passwords-dont-match": "Hasła się nie zgadzają",
"label.profile": "Profil",
"label.realtime": "Czas rzeczywisty",
"label.realtime-logs": "Logi w czasie rzeczywistym",
"label.refresh": "Odśwież",
"label.required": "Wymagany",
"label.reset": "Zresetuj",
"label.save": "Zapisz",
"label.settings": "Ustawienia",
"label.share-url": "Udostępnij adres URL",
"label.single-day": "W tym dniu",
"label.this-month": "W tym miesiącu",
"label.this-week": "W tym tygodniu",
"label.this-year": "W tym roku",
"label.timezone": "Strefa czasowa",
"label.today": "Dzisiaj",
"label.tracking-code": "Kod śledzenia",
"label.unknown": "Nieznany",
"label.username": "Nazwa użytkownika",
"label.view-details": "Pokaż szczegóły",
"label.websites": "Witryny",
"message.active-users": "{x} aktualnie {x, plural, one {odwiedzający} other {odwiedzających}}",
"message.confirm-delete": "Czy na pewno chcesz usunąć {target}?",
"message.copied": "Skopiowano!",
"message.delete-warning": "Wszystkie powiązane dane również zostaną usunięte.",
"message.failure": "Coś poszło nie tak.",
"message.get-share-url": "Uzyskaj adres URL udostępniania",
"message.get-tracking-code": "Pobierz kod śledzenia",
"message.go-to-settings": "Przejdź do ustawień",
"message.incorrect-username-password": "Nieprawidłowa nazwa użytkownika/hasło.",
"message.log.visitor": "Odwiedzający z {country} używa {browser} na {os} {device}",
"message.new-version-available": "Nowa wersja umami {version} jest dostępna!",
"message.no-data-available": "Brak dostępnych danych.",
"message.no-websites-configured": "Nie masz skonfigurowanych żadnych witryn internetowych.",
"message.page-not-found": "Strona nie znaleziona.",
"message.powered-by": "Obsługiwane przez {name}",
"message.save-success": "Zapisano pomyślnie.",
"message.share-url": "To jest publicznie udostępniany adres URL dla {target}.",
"message.track-stats": "Aby śledzić statystyki dla {target}, umieść poniższy kod w sekcji {head} swojej witryny.",
"message.type-delete": "Wpisz {delete} w polu poniżej, aby potwierdzić.",
"metrics.actions": "Działania",
"metrics.average-visit-time": "Średni czas wizyty",
"metrics.bounce-rate": "Współczynnik odrzuceń",
"metrics.browsers": "Przeglądarki",
"metrics.countries": "Kraje",
"metrics.device.desktop": "Komputer",
"metrics.device.laptop": "Laptop",
"metrics.device.mobile": "Smartfon",
"metrics.device.tablet": "Tablet",
"metrics.devices": "Urządzenia",
"metrics.events": "Zdarzenia",
"metrics.filter.combined": "Połączone",
"metrics.filter.domain-only": "Tylko domena",
"metrics.filter.raw": "Surowe dane",
"metrics.operating-systems": "System operacyjny",
"metrics.page-views": "Wyświetlenia strony",
"metrics.pages": "Strony",
"metrics.referrers": "Źródła odsyłające",
"metrics.unique-visitors": "Unikalni odwiedzający",
"metrics.views": "Wyświetlenia",
"metrics.visitors": "Odwiedzający"
}

99
lang/pt-BR.json Normal file
View File

@ -0,0 +1,99 @@
{
"label.accounts": "Contas",
"label.add-account": "Adicionar conta",
"label.add-website": "Adicionar site",
"label.administrator": "Administrador",
"label.all": "Todos",
"label.all-websites": "Todos os sites",
"label.back": "Voltar",
"label.cancel": "Cancelar",
"label.change-password": "Alterar a senha",
"label.confirm-password": "Confirme a nova senha",
"label.copy-to-clipboard": "Copiar para a área de transferência",
"label.current-password": "Senha atual",
"label.custom-range": "Intervalo personalizado",
"label.dashboard": "Painel",
"label.date-range": "Intervalo de datas",
"label.default-date-range": "Intervalo de datas predefinido",
"label.delete": "Remover",
"label.delete-account": "Remover conta",
"label.delete-website": "Remover site",
"label.dismiss": "Dispensar",
"label.domain": "Domínio",
"label.edit": "Editar",
"label.edit-account": "Editar conta",
"label.edit-website": "Editar site",
"label.enable-share-url": "Ativar link de compartilhamento",
"label.invalid": "Inválido",
"label.invalid-domain": "Domínio inválido",
"label.last-days": "Últimos {x} dias",
"label.last-hours": "Últimas {x} horas",
"label.logged-in-as": "Sessão iniciada como {username}",
"label.login": "Iniciar sessão",
"label.logout": "Sair",
"label.more": "Mais",
"label.name": "Nome",
"label.new-password": "Nova senha",
"label.password": "Senha",
"label.passwords-dont-match": "As senhas não correspondem",
"label.profile": "Perfil",
"label.realtime": "Tempo real",
"label.realtime-logs": "Relatório em tempo real",
"label.refresh": "Atualizar",
"label.required": "Obrigatório",
"label.reset": "Redefinir",
"label.save": "Salvar",
"label.settings": "Configurações",
"label.share-url": "Link de compartilhamento",
"label.single-day": "Dia específico",
"label.this-month": "Este mês",
"label.this-week": "Esta semana",
"label.this-year": "Este ano",
"label.timezone": "Fuso horário",
"label.today": "Hoje",
"label.tracking-code": "Código de rastreamento",
"label.unknown": "Desconhecido",
"label.username": "Nome de usuário",
"label.view-details": "Ver detalhes",
"label.websites": "Sites",
"message.active-users": "{x} {x, plural, one {visitante} other {visitantes}} neste momento",
"message.confirm-delete": "Deseja realmente remover {target}?",
"message.copied": "Copiado!",
"message.delete-warning": "Todos os dados associados também serão eliminados.",
"message.failure": "Ocorreu um erro.",
"message.get-share-url": "Obter link de compartilhamento",
"message.get-tracking-code": "Obter código de rastreamento",
"message.go-to-settings": "Ir para as configurações",
"message.incorrect-username-password": "O nome de usuário e/ou senha está incorreto.",
"message.log.visitor": "Visitante de {country} usando {browser} no {device} {os}",
"message.new-version-available": "Uma nova versão de umami {version} está disponível!",
"message.no-data-available": "Sem dados disponíveis.",
"message.no-websites-configured": "Nenhum site foi configurado ainda.",
"message.page-not-found": "Página não encontrada.",
"message.powered-by": "Distribuído por {name}",
"message.save-success": "Salvo com sucesso.",
"message.share-url": "Este é o link público de compartilhamento para {target}.",
"message.track-stats": "Para gerar estatística para {target}, coloque o seguinte código no {head} do html do seu site.",
"message.type-delete": "Escreva {delete} abaixo para continuar.",
"metrics.actions": "Ações",
"metrics.average-visit-time": "Tempo médio da visita",
"metrics.bounce-rate": "Taxa de rejeição",
"metrics.browsers": "Navegadores",
"metrics.countries": "Países",
"metrics.device.desktop": "Computador",
"metrics.device.laptop": "Notebook",
"metrics.device.mobile": "Celular",
"metrics.device.tablet": "Tablet",
"metrics.devices": "Dispositivos",
"metrics.events": "Eventos",
"metrics.filter.combined": "Combinado",
"metrics.filter.domain-only": "Apenas domínio",
"metrics.filter.raw": "Dados brutos",
"metrics.operating-systems": "Sistemas operacionais",
"metrics.page-views": "Visualizações de página",
"metrics.pages": "Páginas",
"metrics.referrers": "Referências",
"metrics.unique-visitors": "Visitantes únicos",
"metrics.views": "Visualizações",
"metrics.visitors": "Visitantes"
}

View File

@ -3,8 +3,8 @@
"label.add-account": "Adicionar conta",
"label.add-website": "Adicionar website",
"label.administrator": "Administrador",
"label.all": "All",
"label.all-websites": "All websites",
"label.all": "Todos",
"label.all-websites": "Todos os websites",
"label.back": "Voltar",
"label.cancel": "Cancelar",
"label.change-password": "Alterar palavra-passe",
@ -37,8 +37,8 @@
"label.password": "Palavra-passe",
"label.passwords-dont-match": "Palavra-passes não correspondem",
"label.profile": "Perfil",
"label.realtime": "Realtime",
"label.realtime-logs": "Realtime logs",
"label.realtime": "Tempo real",
"label.realtime-logs": "Relatório em tempo real",
"label.refresh": "Atualizar",
"label.required": "Obrigatório",
"label.reset": "Repor",
@ -65,7 +65,7 @@
"message.get-tracking-code": "Obter código de tracking",
"message.go-to-settings": "Ir para as definições",
"message.incorrect-username-password": "Nome de utilizador/palavra-passe incorretos.",
"message.log.visitor": "Visitor from {country} using {browser} on {os} {device}",
"message.log.visitor": "Visitante de {country} a usar {browser} no {device} {os}",
"message.new-version-available": "Uma nova versão de umami {version} está disponível!",
"message.no-data-available": "Sem dados disponíveis.",
"message.no-websites-configured": "Não tens nenhum website configurado.",

99
lang/sk-SK.json Normal file
View File

@ -0,0 +1,99 @@
{
"label.accounts": "Účty",
"label.add-account": "Pridať účet",
"label.add-website": "Pridať web",
"label.administrator": "Administrátor",
"label.all": "Všetko",
"label.all-websites": "Všetky weby",
"label.back": "Späť",
"label.cancel": "Zrušiť",
"label.change-password": "Zmeniť heslo",
"label.confirm-password": "Potvrdiť heslo",
"label.copy-to-clipboard": "Kopírovať do schránky",
"label.current-password": "Aktuálne heslo",
"label.custom-range": "Vlastný rozsah",
"label.dashboard": "Prehlad",
"label.date-range": "Obdobie",
"label.default-date-range": "Predvolené obdobie",
"label.delete": "Zmazať",
"label.delete-account": "Zmazať účet",
"label.delete-website": "Zmazať web",
"label.dismiss": "Odísť",
"label.domain": "Doména",
"label.edit": "Upraviť",
"label.edit-account": "Upraviť účet",
"label.edit-website": "Upraviť web",
"label.enable-share-url": "Povoliť zdielanie URL",
"label.invalid": "Neplatný",
"label.invalid-domain": "Neplatná doména",
"label.last-days": "Posledných {x} dní",
"label.last-hours": "Posledných {x} hodín",
"label.logged-in-as": "Prihlásený ako {username}",
"label.login": "Prihlásiť",
"label.logout": "Odhlásiť",
"label.more": "Viac",
"label.name": "Meno",
"label.new-password": "Nové heslo",
"label.password": "Heslo",
"label.passwords-dont-match": "Hesla se nezhodujú",
"label.profile": "Profil",
"label.realtime": "Aktuálne",
"label.realtime-logs": "Aktuálne záznamy",
"label.refresh": "Obnoviť",
"label.required": "Povinné",
"label.reset": "Reset",
"label.save": "Uložiť",
"label.settings": "Nastavenia",
"label.share-url": "Zdielanie URL",
"label.single-day": "Jeden deň",
"label.this-month": "Tento mesiac",
"label.this-week": "Tento týždeň",
"label.this-year": "Tento rok",
"label.timezone": "Časová zóna",
"label.today": "Dnes",
"label.tracking-code": "Sledovací kód",
"label.unknown": "Neznámý",
"label.username": "Užívateľské meno",
"label.view-details": "Zobraziť detaily",
"label.websites": "Weby",
"message.active-users": "{x} aktuálne {x, plural, one {návštevník} other {návštěvníci}}",
"message.confirm-delete": "Naozaj zmazať {target}?",
"message.copied": "Skopírované!",
"message.delete-warning": "Všetky príbuzné data budu tiež zmazané.",
"message.failure": "Niečo sa pokazilo.",
"message.get-share-url": "Získať zdielané URL",
"message.get-tracking-code": "Získať tracking kód",
"message.go-to-settings": "Ísť do nastavení",
"message.incorrect-username-password": "Nesprávné meno/heslo.",
"message.log.visitor": "Návštevník z {country} s prehliadačom {browser} na {os} {device}",
"message.new-version-available": "Nová verzia umami {version} je k dispozícii!",
"message.no-data-available": "Žiadne data.",
"message.no-websites-configured": "Nemáte nastavený žiadny web.",
"message.page-not-found": "Stránka sa nenašla.",
"message.powered-by": "Powered by {name}",
"message.save-success": "Úspešne uložené.",
"message.share-url": "Toto je zdielané URL pre {target}.",
"message.track-stats": "Pre sledovanie návštev na {target}, pridajte následujúci kód do {head} časti vašeho webu.",
"message.type-delete": "Napíšte {delete} pre potvrdenie.",
"metrics.actions": "Akcie",
"metrics.average-visit-time": "Priemerný čas návštevy",
"metrics.bounce-rate": "Okamžité opustenie",
"metrics.browsers": "Prehliadač",
"metrics.countries": "Zem",
"metrics.device.desktop": "Stolný počítač",
"metrics.device.laptop": "Prenosný počítač",
"metrics.device.mobile": "Mobilný telefon",
"metrics.device.tablet": "Tablet",
"metrics.devices": "Zariadenie",
"metrics.events": "Udalosti",
"metrics.filter.combined": "Kombinácie",
"metrics.filter.domain-only": "Domény",
"metrics.filter.raw": "Nezpracované",
"metrics.operating-systems": "Operačný systém",
"metrics.page-views": "Zobrazenie stánok",
"metrics.pages": "Stránky",
"metrics.referrers": "Odkazy",
"metrics.unique-visitors": "Jedinečné návštevy",
"metrics.views": "Zobrazení",
"metrics.visitors": "Návštevy"
}

100
lang/ta-IN.json Normal file
View File

@ -0,0 +1,100 @@
{
"label.accounts": "கணக்குகள்",
"label.add-account": "கணக்கு சேர்க்க",
"label.add-website": "வலைத்தளத்தைச் சேர்க்க",
"label.administrator": "நிர்வாகியைச் சேர்க்க",
"label.all": "எல்லாம்",
"label.all-events": "அனைத்து நிகழ்வுகளும்",
"label.all-websites": "அனைத்து வலைத்தளங்களும்",
"label.back": "பின்னால்",
"label.cancel": "ரத்துசெய்",
"label.change-password": "கடவுச்சொல்லை மாற்று",
"label.confirm-password": "கடவுச்சொல்லை உறுதிப்படுத்தவும்",
"label.copy-to-clipboard": "கிளிப்போர்டுக்கு நகலெடுக்கவும்",
"label.current-password": "தற்போதைய கடவுச்சொல்",
"label.custom-range": "தனிப்பயன் வேறுபாட்டெல்லை",
"label.dashboard": "முகப்பு",
"label.date-range": "தேதி வரம்பு",
"label.default-date-range": "இயல்புநிலை தேதி வரம்பு",
"label.delete": "அழி",
"label.delete-account": "கணக்கை நீக்குக",
"label.delete-website": "வலைத்தளத்தை நீக்கு",
"label.dismiss": "நீக்கு",
"label.domain": "கள முகவரி",
"label.edit": "திருத்துதல்",
"label.edit-account": "கணக்கைத் திருத்து",
"label.edit-website": "வலைத்தளத்தைத் திருத்து",
"label.enable-share-url": "கள முகவரியை பகிரலாம்",
"label.invalid": "தவறானது",
"label.invalid-domain": "தவறான கள முகவரி",
"label.last-days": "முந்தைய {x} நாட்கள்",
"label.last-hours": "முந்தைய {x} மணி",
"label.logged-in-as": "{username} உள்நுழைந்துள்ளீர்",
"label.login": "உள்நுழைய",
"label.logout": "வெளியேறு",
"label.more": "மேலும்",
"label.name": "பெயர்",
"label.new-password": "புதிய கடவுச்சொல்",
"label.password": "கடவுச்சொல்",
"label.passwords-dont-match": "இருக்கடவுச்சொல் பொருந்தவில்லை",
"label.profile": "சுயவிவரம்",
"label.realtime": "தற்போதைய",
"label.realtime-logs": "தற்போதைய பதிவுகள்",
"label.refresh": "புதுப்பிப்பு",
"label.required": "தேவையானவை",
"label.reset": "மீட்டமை",
"label.save": "சேமி",
"label.settings": "அமைப்புகள்",
"label.share-url": "வலைத்தள களத்தைப் பகிரவும்",
"label.single-day": "ஒரு நாள்",
"label.this-month": "இந்த மாதம்",
"label.this-week": "இந்த வாரம்",
"label.this-year": "இந்த வருடம்",
"label.timezone": "நேர மண்டலம்",
"label.today": "இன்று",
"label.tracking-code": "கண்காணிப்பு குறியீடு",
"label.unknown": "தெரியாத",
"label.username": "பயனர்பெயர்",
"label.view-details": "விபரங்களை பார்",
"label.websites": "வலைத்தளங்கள்",
"message.active-users": "{x} தற்போதைய {x, plural, one {ஒன்று} other {மற்ற}}",
"message.confirm-delete": "நீங்கள் நிச்சயமாக {target} நீக்க விரும்புகிறீர்களா?",
"message.copied": "நகலெடுக்கப்பட்டது!",
"message.delete-warning": "தொடர்புடைய எல்லா தரவும் நீக்கப்படும்.",
"message.failure": "ஏதோ தவறு நடந்துவிட்டது.",
"message.get-share-url": "கள முகவரியை ஐப் பெறுக",
"message.get-tracking-code": "கண்காணிப்பு குறியீட்டைப் பெறுக",
"message.go-to-settings": "அமைப்புகளுக்குச் செல்லவும்",
"message.incorrect-username-password": "தவறான பயனர்பெயர் / கடவுச்சொல்.",
"message.log.visitor": "{country}வில் இருந்து பார்வையாளர் {browser} ஐ {os} {device}லில் பயன்படுத்துகிறார்",
"message.new-version-available": "umami {version} இன் புதிய பதிப்பு கிடைக்கும்!",
"message.no-data-available": "தரவு எதுவும் கிடைக்கவில்லை.",
"message.no-websites-configured": "உங்களிடம் எந்த வலைத்தளங்களும் கட்டமைக்கப்படவில்லை.",
"message.page-not-found": "பக்கம் கிடைக்கவில்லை.",
"message.powered-by": "{name} ஆல் இயக்கப்படுகிறது",
"message.save-success": "வெற்றிகரமாக சேமிக்கப்பட்டது.",
"message.share-url": "{target} இது பொதுவில் பகிரும் வலைத்தள முகவரி.",
"message.track-stats": "{target}க்கான புள்ளிவிவரங்களைக் கண்காணிக்க, {head}ல் பின்வரும் குறியீட்டை வைக்கவும்.",
"message.type-delete": "உறுதிப்படுத்த கீழே உள்ள பெட்டியில் {delete} என தட்டச்சு செய்க.",
"metrics.actions": "செயல்கள்",
"metrics.average-visit-time": "சராசரி வருகை நேரம்",
"metrics.bounce-rate": "துள்ளல் விகிதம்",
"metrics.browsers": "உலாவிகள்",
"metrics.countries": "நாடுகள்",
"metrics.device.desktop": "மேசை கணினி",
"metrics.device.laptop": "மடிக்கணினி",
"metrics.device.mobile": "கைபேசி",
"metrics.device.tablet": "கையடக்க கணினி",
"metrics.devices": "சாதனங்கள்",
"metrics.events": "நிகழ்வுகள்",
"metrics.filter.combined": "ஒருங்கிணைந்த",
"metrics.filter.domain-only": "கள முகவரி மட்டும்",
"metrics.filter.raw": "மூல",
"metrics.operating-systems": "இயக்க முறைமைகள்",
"metrics.page-views": "பக்க காட்சிகள்",
"metrics.pages": "பக்கங்கள்",
"metrics.referrers": "குறிப்பிடுவோர்",
"metrics.unique-visitors": "தனிப்பட்ட பார்வையாளர்கள்",
"metrics.views": "பார்வைகள்",
"metrics.visitors": "பார்வையாளர்கள்"
}

View File

@ -1,10 +1,10 @@
{
"label.accounts": "Облікові записи",
"label.add-account": "Додати обліковий запис",
"label.add-website": "Додати вебсайт",
"label.add-website": "Додати сайт",
"label.administrator": "Адміністратор",
"label.all": "Всі",
"label.all-websites": "Всі вебсайти",
"label.all-websites": "Всі сайти",
"label.back": "Назад",
"label.cancel": "Відмінити",
"label.change-password": "Змінити пароль",
@ -14,15 +14,15 @@
"label.custom-range": "Довільний період",
"label.dashboard": "Інформаційна панель",
"label.date-range": "Діапазон дат",
"label.default-date-range": "Діапазон дат за умовчанням",
"label.default-date-range": "Діапазон дат за замовчуванням",
"label.delete": "Видалити",
"label.delete-account": "Видалити обліковий запис",
"label.delete-website": "Видалити вебсайт",
"label.delete-website": "Видалити сайт",
"label.dismiss": "Відхилити",
"label.domain": "Домен",
"label.edit": "Редагувати",
"label.edit-account": "Редагувати обліковий запис",
"label.edit-website": "Редагувати вебсайт",
"label.edit-website": "Редагувати сайт",
"label.enable-share-url": "Дозволити ділитися посиланням",
"label.invalid": "Некоректний",
"label.invalid-domain": "Некоректний домен",
@ -46,16 +46,16 @@
"label.settings": "Налаштування",
"label.share-url": "Поділитися посилання",
"label.single-day": "Один день",
"label.this-month": "Поточний місяць",
"label.this-week": "Поточний тиждень",
"label.this-year": "Поточний рік",
"label.this-month": "Цього місяця",
"label.this-week": "Цього тижня",
"label.this-year": "Цього ріку",
"label.timezone": "Часовий пояс",
"label.today": "Сьогодні",
"label.tracking-code": "Код для відслідковування",
"label.unknown": "Невідомо",
"label.username": "Ім'я користувача",
"label.view-details": "Переглянути деталі",
"label.websites": "Вебсайти",
"label.websites": "Сайти",
"message.active-users": "{x} поточних відвідувачів",
"message.confirm-delete": "Ви впевнені, що бажаєте видалити {target}?",
"message.copied": "Скопійовано!",
@ -68,19 +68,19 @@
"message.log.visitor": "Відвідувач з {country} використовуючи {browser} на {os} {device}",
"message.new-version-available": "Нова версія umami {version} доступна!",
"message.no-data-available": "Немає даних.",
"message.no-websites-configured": "У вас немає налаштованих вебсайтів.",
"message.no-websites-configured": "У вас немає налаштованих сайтів.",
"message.page-not-found": "Сторінку не знайдено.",
"message.powered-by": "На базі {name}",
"message.save-success": "Збережено успішно.",
"message.share-url": "Це публічне посилання для {target}.",
"message.track-stats": "Або відслідковувати статистику для {target}, розмістіть наступний код у {head} секції вашого вебсайту.",
"message.track-stats": "Аби відслідковувати статистику для {target}, розмістіть наступний код у {head} секції вашого сайту.",
"message.type-delete": "Введіть {delete} у полі нижче щоб підтвердити.",
"metrics.actions": "Дії",
"metrics.average-visit-time": "Середній час візиту",
"metrics.bounce-rate": "Показник відмов",
"metrics.browsers": "Браузери",
"metrics.countries": "Країни",
"metrics.device.desktop": "Настільний комп'ютер",
"metrics.device.desktop": "Настільний ПК",
"metrics.device.laptop": "Ноутбук",
"metrics.device.mobile": "Мобільний",
"metrics.device.tablet": "Планшет",
@ -89,7 +89,7 @@
"metrics.filter.combined": "Об'єднані",
"metrics.filter.domain-only": "Лише домен",
"metrics.filter.raw": "Сирі дані",
"metrics.operating-systems": "Операційна система",
"metrics.operating-systems": "Операційні системи",
"metrics.page-views": "Перегляди сторінок",
"metrics.pages": "Сторінки",
"metrics.referrers": "Джерела",

View File

@ -13,8 +13,8 @@
"label.current-password": "目前密码",
"label.custom-range": "自定义时间段",
"label.dashboard": "仪表板",
"label.date-range": "多日",
"label.default-date-range": "默认日期范围",
"label.date-range": "时间段",
"label.default-date-range": "默认时间段",
"label.delete": "删除",
"label.delete-account": "删除账户",
"label.delete-website": "删除网站",
@ -23,7 +23,7 @@
"label.edit": "编辑",
"label.edit-account": "编辑账户",
"label.edit-website": "编辑网站",
"label.enable-share-url": "激活共享链接",
"label.enable-share-url": "启用共享链接",
"label.invalid": "输入无效",
"label.invalid-domain": "无效域名",
"label.last-days": "最近 {x} 天",
@ -57,30 +57,30 @@
"label.view-details": "查看更多",
"label.websites": "网站",
"message.active-users": "当前在线 {x} 人",
"message.confirm-delete": "你确定要删除{target}吗?",
"message.copied": "复制成功!",
"message.delete-warning": "所有相关数据将会被删除.",
"message.failure": "出现错误.",
"message.get-share-url": "获共享链接",
"message.get-tracking-code": "获跟踪代码",
"message.confirm-delete": "你确定要删除 {target} 吗?",
"message.copied": "复制成功",
"message.delete-warning": "所有相关数据将会被删除",
"message.failure": "出现错误",
"message.get-share-url": "获共享链接",
"message.get-tracking-code": "获跟踪代码",
"message.go-to-settings": "去设置",
"message.incorrect-username-password": "用户名密码不正确.",
"message.log.visitor": "来自 {country} 的访客在搭载 {os} 的 {device} 上使用 {browser} 进行访问.",
"message.incorrect-username-password": "用户名或密码不正确。",
"message.log.visitor": "来自 {country} 的访客在搭载 {os} 的 {device} 上使用 {browser} 浏览器进行访问。",
"message.new-version-available": "umami 有新版本 {version} 发布啦!",
"message.no-data-available": "无可用数据.",
"message.no-websites-configured": "你还没有设置任何网站.",
"message.page-not-found": "网页未找到.",
"message.powered-by": "运行 {name}",
"message.save-success": "成功保存.",
"message.share-url": "这是 {target} 的共享链接.",
"message.track-stats": "把以下代码放到你的网站的{head}部分来收集{target}的数据.",
"message.type-delete": "在下面空格输入{delete}确认",
"message.no-data-available": "无可用数据",
"message.no-websites-configured": "你还没有设置任何网站",
"message.page-not-found": "网页未找到",
"message.powered-by": "由 {name} 提供支持",
"message.save-success": "保存成功。",
"message.share-url": "这是 {target} 的共享链接",
"message.track-stats": "把以下代码放到你的网站的 {head} 部分来收集 {target} 的数据。",
"message.type-delete": "在下方输入框输入 {delete} 以确认删除。",
"metrics.actions": "用户行为",
"metrics.average-visit-time": "平均访问时间",
"metrics.bounce-rate": "跳出率",
"metrics.browsers": "浏览器",
"metrics.countries": "国家",
"metrics.device.desktop": "台式机",
"metrics.device.desktop": "桌面电脑",
"metrics.device.laptop": "笔记本",
"metrics.device.mobile": "手机",
"metrics.device.tablet": "平板",
@ -90,10 +90,10 @@
"metrics.filter.domain-only": "只看域名",
"metrics.filter.raw": "原始",
"metrics.operating-systems": "操作系统",
"metrics.page-views": "页面量",
"metrics.page-views": "页面浏览量",
"metrics.pages": "网页",
"metrics.referrers": "指入域名",
"metrics.referrers": "来源域名",
"metrics.unique-visitors": "独立访客",
"metrics.views": "页面流量",
"metrics.visitors": "独立访客"
"metrics.views": "浏览量",
"metrics.visitors": "访客"
}

View File

@ -80,12 +80,14 @@ export const POSTGRESQL_DATE_FORMATS = {
year: 'YYYY-01-01',
};
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(:[1-9]\d{0,4})?|((?=[a-z0-9-]{1,63}\.)(xn--)?[a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{2,63})$/;
export const DESKTOP_SCREEN_WIDTH = 1920;
export const LAPTOP_SCREEN_WIDTH = 1024;
export const MOBILE_SCREEN_WIDTH = 479;
export const URL_LENGTH = 500;
export const DESKTOP_OS = [
'Windows 3.11',
'Windows 95',
@ -116,7 +118,7 @@ export const BROWSERS = {
edge: 'Edge',
'edge-ios': 'Edge (iOS)',
yandexbrowser: 'Yandex',
kakaotalk: 'KKaoTalk',
kakaotalk: 'KaKaoTalk',
samsung: 'Samsung',
silk: 'Silk',
miui: 'MIUI',
@ -140,3 +142,256 @@ export const BROWSERS = {
'ios-webview': 'iOS (webview)',
searchbot: 'Searchbot',
};
export const MAP_FILE = '/datamaps.world.json';
export const ISO_COUNTRIES = {
AFG: 'AF',
ALA: 'AX',
ALB: 'AL',
DZA: 'DZ',
ASM: 'AS',
AND: 'AD',
AGO: 'AO',
AIA: 'AI',
ATA: 'AQ',
ATG: 'AG',
ARG: 'AR',
ARM: 'AM',
ABW: 'AW',
AUS: 'AU',
AUT: 'AT',
AZE: 'AZ',
BHS: 'BS',
BHR: 'BH',
BGD: 'BD',
BRB: 'BB',
BLR: 'BY',
BEL: 'BE',
BLZ: 'BZ',
BEN: 'BJ',
BMU: 'BM',
BTN: 'BT',
BOL: 'BO',
BIH: 'BA',
BWA: 'BW',
BVT: 'BV',
BRA: 'BR',
VGB: 'VG',
IOT: 'IO',
BRN: 'BN',
BGR: 'BG',
BFA: 'BF',
BDI: 'BI',
KHM: 'KH',
CMR: 'CM',
CAN: 'CA',
CPV: 'CV',
CYM: 'KY',
CAF: 'CF',
TCD: 'TD',
CHL: 'CL',
CHN: 'CN',
HKG: 'HK',
MAC: 'MO',
CXR: 'CX',
CCK: 'CC',
COL: 'CO',
COM: 'KM',
COG: 'CG',
COD: 'CD',
COK: 'CK',
CRI: 'CR',
CIV: 'CI',
HRV: 'HR',
CUB: 'CU',
CYP: 'CY',
CZE: 'CZ',
DNK: 'DK',
DJI: 'DJ',
DMA: 'DM',
DOM: 'DO',
ECU: 'EC',
EGY: 'EG',
SLV: 'SV',
GNQ: 'GQ',
ERI: 'ER',
EST: 'EE',
ETH: 'ET',
FLK: 'FK',
FRO: 'FO',
FJI: 'FJ',
FIN: 'FI',
FRA: 'FR',
GUF: 'GF',
PYF: 'PF',
ATF: 'TF',
GAB: 'GA',
GMB: 'GM',
GEO: 'GE',
DEU: 'DE',
GHA: 'GH',
GIB: 'GI',
GRC: 'GR',
GRL: 'GL',
GRD: 'GD',
GLP: 'GP',
GUM: 'GU',
GTM: 'GT',
GGY: 'GG',
GIN: 'GN',
GNB: 'GW',
GUY: 'GY',
HTI: 'HT',
HMD: 'HM',
VAT: 'VA',
HND: 'HN',
HUN: 'HU',
ISL: 'IS',
IND: 'IN',
IDN: 'ID',
IRN: 'IR',
IRQ: 'IQ',
IRL: 'IE',
IMN: 'IM',
ISR: 'IL',
ITA: 'IT',
JAM: 'JM',
JPN: 'JP',
JEY: 'JE',
JOR: 'JO',
KAZ: 'KZ',
KEN: 'KE',
KIR: 'KI',
PRK: 'KP',
KOR: 'KR',
KWT: 'KW',
KGZ: 'KG',
LAO: 'LA',
LVA: 'LV',
LBN: 'LB',
LSO: 'LS',
LBR: 'LR',
LBY: 'LY',
LIE: 'LI',
LTU: 'LT',
LUX: 'LU',
MKD: 'MK',
MDG: 'MG',
MWI: 'MW',
MYS: 'MY',
MDV: 'MV',
MLI: 'ML',
MLT: 'MT',
MHL: 'MH',
MTQ: 'MQ',
MRT: 'MR',
MUS: 'MU',
MYT: 'YT',
MEX: 'MX',
FSM: 'FM',
MDA: 'MD',
MCO: 'MC',
MNG: 'MN',
MNE: 'ME',
MSR: 'MS',
MAR: 'MA',
MOZ: 'MZ',
MMR: 'MM',
NAM: 'NA',
NRU: 'NR',
NPL: 'NP',
NLD: 'NL',
ANT: 'AN',
NCL: 'NC',
NZL: 'NZ',
NIC: 'NI',
NER: 'NE',
NGA: 'NG',
NIU: 'NU',
NFK: 'NF',
MNP: 'MP',
NOR: 'NO',
OMN: 'OM',
PAK: 'PK',
PLW: 'PW',
PSE: 'PS',
PAN: 'PA',
PNG: 'PG',
PRY: 'PY',
PER: 'PE',
PHL: 'PH',
PCN: 'PN',
POL: 'PL',
PRT: 'PT',
PRI: 'PR',
QAT: 'QA',
REU: 'RE',
ROU: 'RO',
RUS: 'RU',
RWA: 'RW',
BLM: 'BL',
SHN: 'SH',
KNA: 'KN',
LCA: 'LC',
MAF: 'MF',
SPM: 'PM',
VCT: 'VC',
WSM: 'WS',
SMR: 'SM',
STP: 'ST',
SAU: 'SA',
SEN: 'SN',
SRB: 'RS',
SYC: 'SC',
SLE: 'SL',
SGP: 'SG',
SVK: 'SK',
SVN: 'SI',
SLB: 'SB',
SOM: 'SO',
ZAF: 'ZA',
SGS: 'GS',
SSD: 'SS',
ESP: 'ES',
LKA: 'LK',
SDN: 'SD',
SUR: 'SR',
SJM: 'SJ',
SWZ: 'SZ',
SWE: 'SE',
CHE: 'CH',
SYR: 'SY',
TWN: 'TW',
TJK: 'TJ',
TZA: 'TZ',
THA: 'TH',
TLS: 'TL',
TGO: 'TG',
TKL: 'TK',
TON: 'TO',
TTO: 'TT',
TUN: 'TN',
TUR: 'TR',
TKM: 'TM',
TCA: 'TC',
TUV: 'TV',
UGA: 'UG',
UKR: 'UA',
ARE: 'AE',
GBR: 'GB',
USA: 'US',
UMI: 'UM',
URY: 'UY',
UZB: 'UZ',
VUT: 'VU',
VEN: 'VE',
VNM: 'VN',
VIR: 'VI',
WLF: 'WF',
ESH: 'EH',
YEM: 'YE',
ZMB: 'ZM',
ZWE: 'ZW',
XKX: 'XK',
};

View File

@ -23,7 +23,10 @@ import {
differenceInCalendarDays,
differenceInCalendarMonths,
differenceInCalendarYears,
format,
} from 'date-fns';
import { enUS } from 'date-fns/locale';
import { dateLocales } from 'lib/lang';
export function getTimezone() {
return moment.tz.guess();
@ -33,8 +36,9 @@ export function getLocalTime(t) {
return addMinutes(new Date(t), new Date().getTimezoneOffset());
}
export function getDateRange(value) {
export function getDateRange(value, locale = 'en-US') {
const now = new Date();
const localeOptions = dateLocales[locale];
const { num, unit } = value.match(/^(?<num>[0-9]+)(?<unit>hour|day|week|month|year)$/).groups;
@ -49,8 +53,8 @@ export function getDateRange(value) {
};
case 'week':
return {
startDate: startOfWeek(now),
endDate: endOfWeek(now),
startDate: startOfWeek(now, { locale: localeOptions }),
endDate: endOfWeek(now, { locale: localeOptions }),
unit: 'day',
value,
};
@ -150,3 +154,16 @@ export function getDateLength(startDate, endDate, unit) {
const [diff] = dateFuncs[unit];
return diff(endDate, startDate) + 1;
}
export const customFormats = {
'en-US': {
p: 'ha',
pp: 'h:mm:ss',
},
};
export function dateFormat(date, str, locale = 'en-US') {
return format(date, customFormats?.[locale]?.[str] || str, {
locale: dateLocales[locale] || enUS,
});
}

View File

@ -113,12 +113,18 @@ export const refFilter = (data, { domain, domainOnly, raw }) => {
return Object.keys(map).map(key => ({ x: key, y: map[key], w: links[key] }));
};
export const browserFilter = data =>
data.map(({ x, y }) => ({ x: BROWSERS[x] || x, y })).filter(({ x }) => x);
export const browserFilter = data => data.map(({ x, y }) => ({ x: BROWSERS[x] ?? x, y }));
export const osFilter = data => data.filter(({ x }) => x);
export const eventTypeFilter = (data, types) => {
if (!types || types.length === 0) {
return data;
}
export const deviceFilter = data => data.filter(({ x }) => x);
return data.filter(({ x }) => {
const [event] = x.split('\t');
return types.some(type => type === event);
});
};
export const percentFilter = data => {
const total = data.reduce((n, { y }) => n + y, 0);

View File

@ -1,24 +1,33 @@
import { format } from 'date-fns';
import {
cs,
sk,
da,
de,
el,
enUS,
es,
fi,
fr,
faIR,
he,
hi,
id,
it,
ja,
ms,
nb,
nl,
pl,
pt,
ptBR,
ro,
ru,
sv,
ta,
tr,
uk,
zhCN,
zhTW,
tr,
ru,
de,
ja,
es,
fr,
da,
sv,
el,
pt,
ro,
nb,
id,
uk,
fi,
} from 'date-fns/locale';
import enMessages from 'lang-compiled/en-US.json';
import nlMessages from 'lang-compiled/nl-NL.json';
@ -36,11 +45,21 @@ import svMessages from 'lang-compiled/sv-SE.json';
import grMessages from 'lang-compiled/el-GR.json';
import foMessages from 'lang-compiled/fo-FO.json';
import ptMessages from 'lang-compiled/pt-PT.json';
import ptBRMessages from 'lang-compiled/pt-BR.json';
import roMessages from 'lang-compiled/ro-RO.json';
import nbNOMessages from 'lang-compiled/nb-NO.json';
import idMessages from 'lang-compiled/id-ID.json';
import ukMessages from 'lang-compiled/uk-UA.json';
import fiMessages from 'lang-compiled/fi-FI.json';
import csMessages from 'lang-compiled/cs-CZ.json';
import skMessages from 'lang-compiled/sk-SK.json';
import plMessages from 'lang-compiled/pl-PL.json';
import taMessages from 'lang-compiled/ta-IN.json';
import hiMessages from 'lang-compiled/hi-IN.json';
import heMessages from 'lang-compiled/he-IL.json';
import itMessages from 'lang-compiled/it-IT.json';
import faIRMessages from 'lang-compiled/fa-IR.json';
import msMYMessages from 'lang-compiled/ms-MY.json';
export const messages = {
'en-US': enMessages,
@ -59,11 +78,21 @@ export const messages = {
'el-GR': grMessages,
'fo-FO': foMessages,
'pt-PT': ptMessages,
'pt-BR': ptBRMessages,
'ro-RO': roMessages,
'nb-NO': nbNOMessages,
'id-ID': idMessages,
'uk-UA': ukMessages,
'fi-FI': fiMessages,
'cs-CZ': csMessages,
'sk-SK': skMessages,
'pl-PL': plMessages,
'ta-IN': taMessages,
'hi-IN': hiMessages,
'he-IL': heMessages,
'it-IT': itMessages,
'fa-IR': faIRMessages,
'ms-MY': msMYMessages,
};
export const dateLocales = {
@ -83,37 +112,53 @@ export const dateLocales = {
'el-GR': el,
'fo-FO': da,
'pt-PT': pt,
'pt-BR': ptBR,
'ro-RO': ro,
'nb-NO': nb,
'id-ID': id,
'uk-UA': uk,
'fi-FI': fi,
'cs-CZ': cs,
'sk-SK': sk,
'pl-PL': pl,
'ta-In': ta,
'hi-IN': hi,
'he-IL': he,
'it-IT': it,
'fa-IR': faIR,
'ms-MY': ms,
};
export const menuOptions = [
{ label: '中文', value: 'zh-CN', display: 'cn' },
{ label: '中文(繁體)', value: 'zh-TW', display: 'tw' },
{ label: 'Čeština', value: 'cs-CZ', display: 'cs' },
{ label: 'Dansk', value: 'da-DK', display: 'da' },
{ label: 'Deutsch', value: 'de-DE', display: 'de' },
{ label: 'English', value: 'en-US', display: 'en' },
{ label: 'Español', value: 'es-MX', display: 'es' },
{ label: 'فارسی', value: 'fa-IR', display: 'fa' },
{ label: 'Føroyskt', value: 'fo-FO', display: 'fo' },
{ label: 'Français', value: 'fr-FR', display: 'fr' },
{ label: 'Ελληνικά', value: 'el-GR', display: 'el' },
{ label: 'עברית', value: 'he-IL', display: 'he' },
{ label: 'हिन्दी', value: 'hi-IN', display: 'hi' },
{ label: 'Italiano', value: 'it-IT', display: 'it' },
{ label: 'Bahasa Indonesia', value: 'id-ID', display: 'id' },
{ label: '日本語', value: 'ja-JP', display: 'ja' },
{ label: 'Malay', value: 'ms-MY', display: 'ms' },
{ label: 'Монгол', value: 'mn-MN', display: 'mn' },
{ label: 'Nederlands', value: 'nl-NL', display: 'nl' },
{ label: 'Norsk Bokmål', value: 'nb-NO', display: 'nb' },
{ label: 'Polski', value: 'pl-PL', display: 'pl' },
{ label: 'Português', value: 'pt-PT', display: 'pt' },
{ label: 'Português do Brasil', value: 'pt-BR', display: 'pt-BR' },
{ label: 'Русский', value: 'ru-RU', display: 'ru' },
{ label: 'Română', value: 'ro-RO', display: 'ro' },
{ label: 'Slovenčina', value: 'sk-SK', display: 'sk' },
{ label: 'Suomi', value: 'fi-FI', display: 'fi' },
{ label: 'Svenska', value: 'sv-SE', display: 'sv' },
{ label: 'தமிழ்', value: 'ta-IN', display: 'ta' },
{ label: 'Türkçe', value: 'tr-TR', display: 'tr' },
{ label: 'українська', value: 'uk-UA', display: 'uk' },
];
export function dateFormat(date, str, locale) {
return format(date, str, { locale: dateLocales[locale] || enUS });
}

View File

@ -1,7 +1,13 @@
import moment from 'moment-timezone';
import prisma from 'lib/db';
import { subMinutes } from 'date-fns';
import { MYSQL, POSTGRESQL, MYSQL_DATE_FORMATS, POSTGRESQL_DATE_FORMATS } from 'lib/constants';
import {
MYSQL,
POSTGRESQL,
MYSQL_DATE_FORMATS,
POSTGRESQL_DATE_FORMATS,
URL_LENGTH,
} from 'lib/constants';
export function getDatabase() {
const type =
@ -68,7 +74,7 @@ export function getTimestampInterval(field) {
export async function getWebsiteById(website_id) {
return runQuery(
prisma.website.findOne({
prisma.website.findUnique({
where: {
website_id,
},
@ -78,7 +84,7 @@ export async function getWebsiteById(website_id) {
export async function getWebsiteByUuid(website_uuid) {
return runQuery(
prisma.website.findOne({
prisma.website.findUnique({
where: {
website_uuid,
},
@ -88,7 +94,7 @@ export async function getWebsiteByUuid(website_uuid) {
export async function getWebsiteByShareId(share_id) {
return runQuery(
prisma.website.findOne({
prisma.website.findUnique({
where: {
share_id,
},
@ -152,11 +158,7 @@ export async function createSession(website_id, data) {
return runQuery(
prisma.session.create({
data: {
website: {
connect: {
website_id,
},
},
website_id,
...data,
},
select: {
@ -168,7 +170,7 @@ export async function createSession(website_id, data) {
export async function getSessionByUuid(session_uuid) {
return runQuery(
prisma.session.findOne({
prisma.session.findUnique({
where: {
session_uuid,
},
@ -180,18 +182,10 @@ export async function savePageView(website_id, session_id, url, referrer) {
return runQuery(
prisma.pageview.create({
data: {
website: {
connect: {
website_id,
},
},
session: {
connect: {
session_id,
},
},
url,
referrer,
website_id,
session_id,
url: url?.substr(0, URL_LENGTH),
referrer: referrer?.substr(0, URL_LENGTH),
},
}),
);
@ -201,19 +195,11 @@ export async function saveEvent(website_id, session_id, url, event_type, event_v
return runQuery(
prisma.event.create({
data: {
website: {
connect: {
website_id,
},
},
session: {
connect: {
session_id,
},
},
url,
event_type,
event_value,
website_id,
session_id,
url: url?.substr(0, URL_LENGTH),
event_type: event_type?.substr(0, 50),
event_value: event_value?.substr(0, 50),
},
}),
);
@ -225,7 +211,7 @@ export async function getAccounts() {
export async function getAccountById(user_id) {
return runQuery(
prisma.account.findOne({
prisma.account.findUnique({
where: {
user_id,
},
@ -235,7 +221,7 @@ export async function getAccountById(user_id) {
export async function getAccountByUsername(username) {
return runQuery(
prisma.account.findOne({
prisma.account.findUnique({
where: {
username,
},
@ -428,7 +414,7 @@ export function getPageviewMetrics(website_id, start_at, end_at, field, table, f
if (domain) {
domainFilter = `and referrer not like $${params.length + 1} and referrer not like '/%'`;
params.push(`%${domain}%`);
params.push(`%://${domain}/%`);
}
if (url) {

View File

@ -6,6 +6,7 @@ module.exports = {
VERSION: pkg.version,
FORCE_SSL: !!process.env.FORCE_SSL,
},
basePath: process.env.BASE_PATH,
webpack(config) {
config.module.rules.push({
test: /\.svg$/,
@ -28,6 +29,6 @@ module.exports = {
},
],
},
]
];
},
};

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