Merge pull request #1030 from mikecao/dev

v1.28.0
pull/1034/head v1.28.0
Mike Cao 2022-03-17 21:26:11 -07:00 committed by GitHub
commit 2b4ddb5388
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
93 changed files with 1092 additions and 1399 deletions

View File

@ -16,7 +16,8 @@
"rules": { "rules": {
"react/display-name": "off", "react/display-name": "off",
"react/react-in-jsx-scope": "off", "react/react-in-jsx-scope": "off",
"react/prop-types": "off" "react/prop-types": "off",
"import/no-anonymous-default-export": "off"
}, },
"globals": { "globals": {
"React": "writable" "React": "writable"

View File

@ -11,6 +11,7 @@ function MenuButton({
value, value,
options, options,
buttonClassName, buttonClassName,
buttonVariant,
menuClassName, menuClassName,
menuPosition = 'bottom', menuPosition = 'bottom',
menuAlign = 'right', menuAlign = 'right',
@ -43,7 +44,7 @@ function MenuButton({
icon={icon} icon={icon}
className={classNames(styles.button, buttonClassName, { [styles.open]: showMenu })} className={classNames(styles.button, buttonClassName, { [styles.open]: showMenu })}
onClick={toggleMenu} onClick={toggleMenu}
variant="light" variant={buttonVariant}
> >
{!hideLabel && ( {!hideLabel && (
<div className={styles.text}>{renderValue ? renderValue(selectedOption) : value}</div> <div className={styles.text}>{renderValue ? renderValue(selectedOption) : value}</div>

View File

@ -1,3 +1,4 @@
import classNames from 'classnames';
import Link from './Link'; import Link from './Link';
import Button from './Button'; import Button from './Button';
import XMark from 'assets/xmark.svg'; import XMark from 'assets/xmark.svg';
@ -5,9 +6,9 @@ import styles from './MobileMenu.module.css';
export default function MobileMenu({ items = [], onClose }) { export default function MobileMenu({ items = [], onClose }) {
return ( return (
<div className={styles.menu}> <div className={classNames(styles.menu, 'container')}>
<div className={styles.header}> <div className={styles.header}>
<Button className={styles.button} icon={<XMark />} onClick={onClose} /> <Button icon={<XMark />} onClick={onClose} />
</div> </div>
<div className={styles.items}> <div className={styles.items}>
{items.map(({ label, value }) => ( {items.map(({ label, value }) => (

View File

@ -32,13 +32,10 @@
margin-top: 60px; margin-top: 60px;
} }
.button {
margin-right: 15px;
}
.header { .header {
display: flex; display: flex;
justify-content: flex-end; justify-content: flex-end;
align-items: center; align-items: center;
height: 100px; height: 100px;
padding: 0 30px;
} }

View File

@ -34,7 +34,7 @@ export default function UpdateNotice() {
values={{ version: `v${latest}` }} values={{ version: `v${latest}` }}
/> />
</div> </div>
<ButtonLayout> <ButtonLayout className={styles.buttons}>
<Button size="xsmall" variant="action" onClick={handleViewClick}> <Button size="xsmall" variant="action" onClick={handleViewClick}>
<FormattedMessage id="label.view-details" defaultMessage="View details" /> <FormattedMessage id="label.view-details" defaultMessage="View details" />
</Button> </Button>

View File

@ -2,12 +2,17 @@
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
padding-top: 10px; padding-top: 20px;
font-size: var(--font-size-small);
font-weight: 600;
} }
.message { .message {
font-size: var(--font-size-small);
font-weight: 600;
flex: 1;
text-align: center; text-align: center;
margin-right: 20px; margin-right: 20px;
} }
.buttons {
flex: 0;
}

View File

@ -1,3 +1,4 @@
import { useRouter } from 'next/router';
import { FormattedMessage } from 'react-intl'; import { FormattedMessage } from 'react-intl';
import classNames from 'classnames'; import classNames from 'classnames';
import Link from 'components/common/Link'; import Link from 'components/common/Link';
@ -14,6 +15,7 @@ import { HOMEPAGE_URL } from 'lib/constants';
export default function Header() { export default function Header() {
const { user } = useUser(); const { user } = useUser();
const { pathname } = useRouter();
return ( return (
<> <>
@ -21,7 +23,7 @@ export default function Header() {
<header className={classNames(styles.header, 'row')}> <header className={classNames(styles.header, 'row')}>
<div className={styles.title}> <div className={styles.title}>
<Icon icon={<Logo />} size="large" className={styles.logo} /> <Icon icon={<Logo />} size="large" className={styles.logo} />
<Link href={user ? '/' : HOMEPAGE_URL}>umami</Link> <Link href={pathname.includes('/share') ? HOMEPAGE_URL : '/'}>umami</Link>
</div> </div>
<HamburgerButton /> <HamburgerButton />
{user && ( {user && (

View File

@ -6,6 +6,7 @@
} }
.title { .title {
flex: 1;
font-size: var(--font-size-large); font-size: var(--font-size-large);
display: flex; display: flex;
align-items: center; align-items: center;
@ -17,7 +18,7 @@
} }
.links { .links {
flex: 1; flex: 2;
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
@ -30,6 +31,7 @@
} }
.buttons { .buttons {
flex: 1;
display: flex; display: flex;
justify-content: flex-end; justify-content: flex-end;
align-items: center; align-items: center;
@ -39,6 +41,7 @@
.header .buttons { .header .buttons {
flex: 1; flex: 1;
} }
.links { .links {
order: 2; order: 2;
margin: 20px 0; margin: 20px 0;
@ -48,7 +51,7 @@
@media only screen and (max-width: 768px) { @media only screen and (max-width: 768px) {
.header { .header {
padding: 0 15px; padding: 0 30px;
} }
.buttons, .buttons,

View File

@ -14,9 +14,7 @@ export default function Layout({ title, children, header = true, footer = true }
</Head> </Head>
{header && <Header />} {header && <Header />}
<main className="container" dir={dir}> <main>{children}</main>
{children}
</main>
{footer && <Footer />} {footer && <Footer />}
<div id="__modals" dir={dir} /> <div id="__modals" dir={dir} />
</> </>

View File

@ -9,7 +9,8 @@ import styles from './ActiveUsers.module.css';
export default function ActiveUsers({ websiteId, className, value, interval = 60000 }) { export default function ActiveUsers({ websiteId, className, value, interval = 60000 }) {
const shareToken = useShareToken(); const shareToken = useShareToken();
const { data } = useFetch(!value && `/website/${websiteId}/active`, { const url = value !== undefined && websiteId ? `/website/${websiteId}/active` : null;
const { data } = useFetch(url, {
interval, interval,
headers: { [TOKEN_HEADER]: shareToken?.token }, headers: { [TOKEN_HEADER]: shareToken?.token },
}); });

View File

@ -10,10 +10,11 @@ import useShareToken from 'hooks/useShareToken';
import { EVENT_COLORS, TOKEN_HEADER } from 'lib/constants'; import { EVENT_COLORS, TOKEN_HEADER } from 'lib/constants';
export default function EventsChart({ websiteId, className, token }) { export default function EventsChart({ websiteId, className, token }) {
const [dateRange] = useDateRange(websiteId); const [{ startDate, endDate, unit, modified }] = useDateRange(websiteId);
const { startDate, endDate, unit, modified } = dateRange;
const [timezone] = useTimezone(); const [timezone] = useTimezone();
const { query } = usePageQuery(); const {
query: { url, eventType },
} = usePageQuery();
const shareToken = useShareToken(); const shareToken = useShareToken();
const { data, loading } = useFetch( const { data, loading } = useFetch(
@ -24,12 +25,13 @@ export default function EventsChart({ websiteId, className, token }) {
end_at: +endDate, end_at: +endDate,
unit, unit,
tz: timezone, tz: timezone,
url: query.url, url,
event_type: eventType,
token, token,
}, },
headers: { [TOKEN_HEADER]: shareToken?.token }, headers: { [TOKEN_HEADER]: shareToken?.token },
}, },
[modified], [modified, eventType],
); );
const datasets = useMemo(() => { const datasets = useMemo(() => {

View File

@ -4,16 +4,18 @@ import MetricsTable from './MetricsTable';
import Tag from 'components/common/Tag'; import Tag from 'components/common/Tag';
import DropDown from 'components/common/DropDown'; import DropDown from 'components/common/DropDown';
import { eventTypeFilter } from 'lib/filters'; import { eventTypeFilter } from 'lib/filters';
import usePageQuery from 'hooks/usePageQuery';
import styles from './EventsTable.module.css'; import styles from './EventsTable.module.css';
const EVENT_FILTER_DEFAULT = { const EVENT_FILTER_DEFAULT = {
value: 'EVENT_FILTER_DEFAULT', value: 'all',
label: <FormattedMessage id="label.all-events" defaultMessage="All events" />, label: <FormattedMessage id="label.all-events" defaultMessage="All events" />,
}; };
export default function EventsTable({ websiteId, ...props }) { export default function EventsTable({ websiteId, ...props }) {
const [eventType, setEventType] = useState(EVENT_FILTER_DEFAULT.value); const [eventType, setEventType] = useState(EVENT_FILTER_DEFAULT.value);
const [eventTypes, setEventTypes] = useState([]); const [eventTypes, setEventTypes] = useState([]);
const { resolve, router } = usePageQuery();
const dropDownOptions = [EVENT_FILTER_DEFAULT, ...eventTypes.map(t => ({ value: t, label: t }))]; const dropDownOptions = [EVENT_FILTER_DEFAULT, ...eventTypes.map(t => ({ value: t, label: t }))];
@ -22,11 +24,16 @@ export default function EventsTable({ websiteId, ...props }) {
props.onDataLoad?.(data); props.onDataLoad?.(data);
} }
function handleChange(value) {
router.replace(resolve({ eventType: value === 'all' ? undefined : value }));
setEventType(value);
}
return ( return (
<> <>
{eventTypes?.length > 1 && ( {eventTypes?.length > 1 && (
<div className={styles.filter}> <div className={styles.filter}>
<DropDown value={eventType} options={dropDownOptions} onChange={setEventType} /> <DropDown value={eventType} options={dropDownOptions} onChange={handleChange} />
</div> </div>
)} )}
<MetricsTable <MetricsTable

View File

@ -32,11 +32,11 @@ export default function WebsiteHeader({ websiteId, title, domain, showLink = fal
return ( return (
<PageHeader className="row"> <PageHeader className="row">
<div className={classNames(styles.title, 'col-12 col-lg-4')}>{header}</div> <div className={classNames(styles.title, 'col-10 col-lg-4 order-1 order-lg-1')}>{header}</div>
<div className={classNames(styles.active, 'col-6 col-lg-4')}> <div className={classNames(styles.active, 'col-12 col-lg-4 order-3 order-lg-2')}>
<ActiveUsers websiteId={websiteId} /> <ActiveUsers websiteId={websiteId} />
</div> </div>
<div className="col-6 col-lg-4"> <div className="col-2 col-lg-4 order-2 order-lg-3">
<ButtonLayout align="right"> <ButtonLayout align="right">
<RefreshButton websiteId={websiteId} /> <RefreshButton websiteId={websiteId} />
{showLink && ( {showLink && (

View File

@ -25,4 +25,8 @@
.active { .active {
justify-content: flex-start; justify-content: flex-start;
} }
a.link {
display: none;
}
} }

View File

@ -0,0 +1,46 @@
import { useState } from 'react';
import { FormattedMessage } from 'react-intl';
import { useRouter } from 'next/router';
import Page from 'components/layout/Page';
import PageHeader from 'components/layout/PageHeader';
import WebsiteList from 'components/pages/WebsiteList';
import Button from 'components/common/Button';
import DashboardSettingsButton from 'components/settings/DashboardSettingsButton';
import useFetch from 'hooks/useFetch';
import useStore from 'store/app';
import styles from './WebsiteList.module.css';
const selector = state => state.dashboard;
export default function Dashboard() {
const router = useRouter();
const { id } = router.query;
const userId = id?.[0];
const store = useStore(selector);
const { showCharts, limit } = store;
const [max, setMax] = useState(limit);
const { data } = useFetch('/websites', { params: { user_id: userId } });
function handleMore() {
setMax(max + limit);
}
if (!data) {
return null;
}
return (
<Page>
<PageHeader>
<div>Dashboard</div>
<DashboardSettingsButton />
</PageHeader>
<WebsiteList websites={data} showCharts={showCharts} limit={max} />
{max < data.length && (
<Button className={styles.button} onClick={handleMore}>
<FormattedMessage id="label.more" defaultMessage="More" />
</Button>
)}
</Page>
);
}

View File

@ -44,7 +44,13 @@ export default function TestConsole() {
<Page> <Page>
<Head> <Head>
{typeof window !== 'undefined' && website && ( {typeof window !== 'undefined' && website && (
<script async defer data-website-id={website.website_uuid} src={`${basePath}/umami.js`} /> <script
async
defer
data-website-id={website.website_uuid}
src={`${basePath}/umami.js`}
data-cache="true"
/>
)} )}
</Head> </Head>
<PageHeader> <PageHeader>
@ -68,7 +74,7 @@ export default function TestConsole() {
{show && ( {show && (
<div className={classNames(styles.test, 'row')}> <div className={classNames(styles.test, 'row')}>
<div className="col-4"> <div className="col-4">
<PageHeader>Page links</PageHeader>Nmo <PageHeader>Page links</PageHeader>
<div> <div>
<Link href={`?page=1`}> <Link href={`?page=1`}>
<a>page one</a> <a>page one</a>

View File

@ -1,32 +1,13 @@
import React, { useState } from 'react';
import { FormattedMessage } from 'react-intl'; import { FormattedMessage } from 'react-intl';
import Link from 'components/common/Link'; import Link from 'components/common/Link';
import WebsiteChart from 'components/metrics/WebsiteChart'; import WebsiteChart from 'components/metrics/WebsiteChart';
import Page from 'components/layout/Page'; import Page from 'components/layout/Page';
import EmptyPlaceholder from 'components/common/EmptyPlaceholder'; import EmptyPlaceholder from 'components/common/EmptyPlaceholder';
import useFetch from 'hooks/useFetch';
import DashboardSettingsButton from 'components/settings/DashboardSettingsButton';
import Button from 'components/common/Button';
import useStore from 'store/app';
import Arrow from 'assets/arrow-right.svg'; import Arrow from 'assets/arrow-right.svg';
import styles from './WebsiteList.module.css'; import styles from './WebsiteList.module.css';
const selector = state => state.dashboard; export default function WebsiteList({ websites, showCharts, limit }) {
if (websites.length === 0) {
export default function WebsiteList({ userId }) {
const { data } = useFetch('/websites', { params: { user_id: userId } });
const { showCharts, limit } = useStore(selector);
const [max, setMax] = useState(limit);
function handleMore() {
setMax(max + limit);
}
if (!data) {
return null;
}
if (data.length === 0) {
return ( return (
<Page> <Page>
<EmptyPlaceholder <EmptyPlaceholder
@ -46,12 +27,9 @@ export default function WebsiteList({ userId }) {
} }
return ( return (
<Page> <div>
<div className={styles.menubar}> {websites.map(({ website_id, name, domain }, index) =>
<DashboardSettingsButton /> index < limit ? (
</div>
{data.map(({ website_id, name, domain }, index) =>
index < max ? (
<div key={website_id} className={styles.website}> <div key={website_id} className={styles.website}>
<WebsiteChart <WebsiteChart
websiteId={website_id} websiteId={website_id}
@ -63,11 +41,6 @@ export default function WebsiteList({ userId }) {
</div> </div>
) : null, ) : null,
)} )}
{max < data.length && ( </div>
<Button className={styles.button} onClick={handleMore}>
<FormattedMessage id="label.more" defaultMessage="More" />
</Button>
)}
</Page>
); );
} }

View File

@ -7,17 +7,5 @@
.website:last-child { .website:last-child {
border-bottom: 0; border-bottom: 0;
margin-bottom: 0; margin-bottom: 20px;
}
.menubar {
display: flex;
align-items: center;
justify-content: flex-end;
padding-top: 10px;
}
.button {
align-self: center;
margin-bottom: 40px;
} }

View File

@ -2,7 +2,7 @@ import React from 'react';
import { FormattedMessage } from 'react-intl'; import { FormattedMessage } from 'react-intl';
import MenuButton from 'components/common/MenuButton'; import MenuButton from 'components/common/MenuButton';
import Gear from 'assets/gear.svg'; import Gear from 'assets/gear.svg';
import useStore, { setDashboard } from 'store/app'; import useStore, { setDashboard, defaultDashboardConfig } from 'store/app';
const selector = state => state.dashboard; const selector = state => state.dashboard;
@ -18,7 +18,7 @@ export default function DashboardSettingsButton() {
function handleSelect(value) { function handleSelect(value) {
if (value === 'charts') { if (value === 'charts') {
setDashboard({ showCharts: !settings.showCharts }); setDashboard({ ...defaultDashboardConfig, showCharts: !settings.showCharts });
} }
//setDashboard(value); //setDashboard(value);
} }

View File

@ -19,6 +19,7 @@ export default function LanguageButton() {
options={menuOptions} options={menuOptions}
value={locale} value={locale}
menuClassName={styles.menu} menuClassName={styles.menu}
buttonVariant="light"
onSelect={handleSelect} onSelect={handleSelect}
hideLabel hideLabel
/> />

View File

@ -4,7 +4,6 @@ import { useRouter } from 'next/router';
import MenuButton from 'components/common/MenuButton'; import MenuButton from 'components/common/MenuButton';
import Icon from 'components/common/Icon'; import Icon from 'components/common/Icon';
import User from 'assets/user.svg'; import User from 'assets/user.svg';
import Chevron from 'assets/chevron-down.svg';
import styles from './UserButton.module.css'; import styles from './UserButton.module.css';
import { removeItem } from 'lib/web'; import { removeItem } from 'lib/web';
import { AUTH_TOKEN } from 'lib/constants'; import { AUTH_TOKEN } from 'lib/constants';
@ -42,9 +41,10 @@ export default function UserButton() {
return ( return (
<MenuButton <MenuButton
icon={<Icon icon={<User />} size="large" />} icon={<Icon icon={<User />} size="large" />}
value={<Icon icon={<Chevron />} size="small" />} buttonVariant="light"
options={menuOptions} options={menuOptions}
onSelect={handleSelect} onSelect={handleSelect}
hideLabel
/> />
); );
} }

View File

@ -9,6 +9,7 @@
"metrics.device.tablet", "metrics.device.tablet",
"metrics.referrers" "metrics.referrers"
], ],
"en-GB": "*",
"fr-FR": ["metrics.actions", "metrics.pages"], "fr-FR": ["metrics.actions", "metrics.pages"],
"lt-LT": [ "lt-LT": [
"metrics.device.desktop", "metrics.device.desktop",
@ -31,6 +32,6 @@
"message.powered-by", "message.powered-by",
"metrics.device.desktop", "metrics.device.desktop",
"metrics.device.tablet", "metrics.device.tablet",
"metrics.filter.raw", "metrics.filter.raw"
], ]
} }

View File

@ -84,7 +84,7 @@
"message.toggle-charts": "Toggle charts", "message.toggle-charts": "Toggle charts",
"message.track-stats": "لتتبع الاحصاىيات لـ {target}, ضع الكود التالي في منطقة {head} في موقعك.", "message.track-stats": "لتتبع الاحصاىيات لـ {target}, ضع الكود التالي في منطقة {head} في موقعك.",
"message.type-delete": "اكتب {delete} في الحقل التالي لتأكيد الحذف.", "message.type-delete": "اكتب {delete} في الحقل التالي لتأكيد الحذف.",
"message.type-reset": "Type {reset} in the box below to confirm.", "message.type-reset": "اكتب {reset} في الحقل التالي لتأكيد الحذف.",
"metrics.actions": "اجراءات", "metrics.actions": "اجراءات",
"metrics.average-visit-time": "متوسط وقت الزيارة", "metrics.average-visit-time": "متوسط وقت الزيارة",
"metrics.bounce-rate": "معدل الارتداد", "metrics.bounce-rate": "معدل الارتداد",

View File

@ -84,7 +84,7 @@
"message.toggle-charts": "Toggle charts", "message.toggle-charts": "Toggle charts",
"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.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í.", "message.type-delete": "Napište {delete} pro potvrzení.",
"message.type-reset": "Type {reset} in the box below to confirm.", "message.type-reset": "Napište {reset} pro potvrzení.",
"metrics.actions": "Akce", "metrics.actions": "Akce",
"metrics.average-visit-time": "Průměrný čas návštěvy", "metrics.average-visit-time": "Průměrný čas návštěvy",
"metrics.bounce-rate": "Okamžité opuštění", "metrics.bounce-rate": "Okamžité opuštění",

View File

@ -84,7 +84,7 @@
"message.toggle-charts": "Toggle charts", "message.toggle-charts": "Toggle charts",
"message.track-stats": "For at spore statistik for {target} skal du placere følgende kode i {head} sektionen på dit websted.", "message.track-stats": "For at spore statistik for {target} skal du placere følgende kode i {head} sektionen på dit websted.",
"message.type-delete": "Skriv {delete} i boksen nedenfor, for at bekræfte.", "message.type-delete": "Skriv {delete} i boksen nedenfor, for at bekræfte.",
"message.type-reset": "Type {reset} in the box below to confirm.", "message.type-reset": "Skriv {reset} i boksen nedenfor, for at bekræfte.",
"metrics.actions": "Handlinger", "metrics.actions": "Handlinger",
"metrics.average-visit-time": "Gennemsnitlig besøgstid", "metrics.average-visit-time": "Gennemsnitlig besøgstid",
"metrics.bounce-rate": "Afvisningsprocent", "metrics.bounce-rate": "Afvisningsprocent",

View File

@ -84,7 +84,7 @@
"message.toggle-charts": "Toggle charts", "message.toggle-charts": "Toggle charts",
"message.track-stats": "Για να παρακολουθείτε στατιστικά στοιχεία για {target}, τοποθετήστε τον ακόλουθο κώδικα στην ενότητα {head} του ιστότοπού σας.", "message.track-stats": "Για να παρακολουθείτε στατιστικά στοιχεία για {target}, τοποθετήστε τον ακόλουθο κώδικα στην ενότητα {head} του ιστότοπού σας.",
"message.type-delete": "Πληκτρολογήστε {delete} στο παρακάτω πλαίσιο για επιβεβαίωση.", "message.type-delete": "Πληκτρολογήστε {delete} στο παρακάτω πλαίσιο για επιβεβαίωση.",
"message.type-reset": "Type {reset} in the box below to confirm.", "message.type-reset": "Πληκτρολογήστε {reset} στο παρακάτω πλαίσιο για επιβεβαίωση.",
"metrics.actions": "Ενέργειες", "metrics.actions": "Ενέργειες",
"metrics.average-visit-time": "Μέσος χρόνος επίσκεψης", "metrics.average-visit-time": "Μέσος χρόνος επίσκεψης",
"metrics.bounce-rate": "Ποσοστό αναπήδησης", "metrics.bounce-rate": "Ποσοστό αναπήδησης",

View File

@ -84,7 +84,7 @@
"message.toggle-charts": "Toggle charts", "message.toggle-charts": "Toggle charts",
"message.track-stats": "برای ردیابی آمار {target}, کد روبرو را در قسمت {head} وب‌سایت قرار دهید.", "message.track-stats": "برای ردیابی آمار {target}, کد روبرو را در قسمت {head} وب‌سایت قرار دهید.",
"message.type-delete": "جهت اطمینان '{delete}' را در کادر زیر بنویسید.", "message.type-delete": "جهت اطمینان '{delete}' را در کادر زیر بنویسید.",
"message.type-reset": "Type {reset} in the box below to confirm.", "message.type-reset": "جهت اطمینان '{reset}' را در کادر زیر بنویسید.",
"metrics.actions": "اقدامات", "metrics.actions": "اقدامات",
"metrics.average-visit-time": "میانگین زمان بازدید", "metrics.average-visit-time": "میانگین زمان بازدید",
"metrics.bounce-rate": "نرخ Bounce", "metrics.bounce-rate": "نرخ Bounce",

View File

@ -4,8 +4,8 @@
"label.add-website": "Lisää verkkosivu", "label.add-website": "Lisää verkkosivu",
"label.administrator": "Järjestelmänvalvoja", "label.administrator": "Järjestelmänvalvoja",
"label.all": "Kaikki", "label.all": "Kaikki",
"label.all-events": "All events", "label.all-events": "Kaikki tapahtumat",
"label.all-time": "All time", "label.all-time": "Alusta lähtien",
"label.all-websites": "Kaikki verkkosivut", "label.all-websites": "Kaikki verkkosivut",
"label.back": "Takaisin", "label.back": "Takaisin",
"label.cancel": "Peruuta", "label.cancel": "Peruuta",
@ -13,7 +13,7 @@
"label.confirm-password": "Vahvista salasana", "label.confirm-password": "Vahvista salasana",
"label.copy-to-clipboard": "Kopioi leikepöydälle", "label.copy-to-clipboard": "Kopioi leikepöydälle",
"label.current-password": "Nykyinen salasana", "label.current-password": "Nykyinen salasana",
"label.custom-range": "Mukautettu jakso", "label.custom-range": "Mukautettu ajanjakso",
"label.dashboard": "Ohjauspaneeli", "label.dashboard": "Ohjauspaneeli",
"label.date-range": "Ajanjakso", "label.date-range": "Ajanjakso",
"label.default-date-range": "Oletusajanjakso", "label.default-date-range": "Oletusajanjakso",
@ -28,30 +28,30 @@
"label.enable-share-url": "Ota jakamisen URL-osoite käyttöön", "label.enable-share-url": "Ota jakamisen URL-osoite käyttöön",
"label.invalid": "Virheellinen", "label.invalid": "Virheellinen",
"label.invalid-domain": "Virheellinen verkkotunnus", "label.invalid-domain": "Virheellinen verkkotunnus",
"label.language": "Language", "label.language": "Kieli",
"label.last-days": "Viimeisimmät {x} päivät", "label.last-days": "Viimeisimmät {x} päivää",
"label.last-hours": "Viimeisimmät {x} tunnit", "label.last-hours": "Viimeisimmät {x} tuntia",
"label.logged-in-as": "Kirjautuneena sisään nimellä {username}", "label.logged-in-as": "Kirjautuneena sisään nimellä {username}",
"label.login": "Kirjaudu sisään", "label.login": "Kirjaudu sisään",
"label.logout": "Kirjaudu ulos", "label.logout": "Kirjaudu ulos",
"label.more": "Lisää", "label.more": "Lisää",
"label.name": "Nimi", "label.name": "Nimi",
"label.new-password": "Uusi salasana", "label.new-password": "Uusi salasana",
"label.owner": "Owner", "label.owner": "Omistaja",
"label.password": "Salasana", "label.password": "Salasana",
"label.passwords-dont-match": "Salasanat eivät täsmää", "label.passwords-dont-match": "Salasanat eivät täsmää",
"label.profile": "Profiili", "label.profile": "Profiili",
"label.realtime": "Reaaliaikainen", "label.realtime": "Juuri nyt",
"label.realtime-logs": "Reaaliaikaiset lokit", "label.realtime-logs": "Reaaliaikaiset lokit",
"label.refresh": "Päivitä", "label.refresh": "Päivitä",
"label.required": "Vaaditaan", "label.required": "Vaaditaan",
"label.reset": "Nollaa", "label.reset": "Nollaa",
"label.reset-website": "Reset statistics", "label.reset-website": "Nollaa tilastot",
"label.save": "Tallenna", "label.save": "Tallenna",
"label.settings": "Asetukset", "label.settings": "Asetukset",
"label.share-url": "Jaa URL", "label.share-url": "Jaa URL",
"label.single-day": "Yksi päivä", "label.single-day": "Yksi päivä",
"label.theme": "Theme", "label.theme": "Teema",
"label.this-month": "Tämä kuukausi", "label.this-month": "Tämä kuukausi",
"label.this-week": "Tämä viikko", "label.this-week": "Tämä viikko",
"label.this-year": "Tämä vuosi", "label.this-year": "Tämä vuosi",
@ -62,29 +62,29 @@
"label.username": "Käyttäjänimi", "label.username": "Käyttäjänimi",
"label.view-details": "Katso tiedot", "label.view-details": "Katso tiedot",
"label.websites": "Verkkosivut", "label.websites": "Verkkosivut",
"message.active-users": "{x} nykyinen {x, plural, one {yksi} other {muut}}", "message.active-users": "{x} {x, plural, one {vierailija} other {vierailijaa}}",
"message.confirm-delete": "Haluatko varmasti poistaa {target}?", "message.confirm-delete": "Haluatko varmasti poistaa sivuston {target}?",
"message.confirm-reset": "Are your sure you want to reset {target}'s statistics?", "message.confirm-reset": "Haluatko varmasti poistaa sivuston {target} tilastot?",
"message.copied": "Kopioitu!", "message.copied": "Kopioitu!",
"message.delete-warning": "Kaikki siihen liittyvät tiedot poistetaan.", "message.delete-warning": "Kaikki siihen liittyvät tiedot poistetaan.",
"message.failure": "Jotain meni väärin.", "message.failure": "Jotain meni pieleen.",
"message.get-share-url": "Hanki jakamisen URL-osoite", "message.get-share-url": "Hanki jakamisen URL-osoite",
"message.get-tracking-code": "Hanki seurantakoodi", "message.get-tracking-code": "Hanki seurantakoodi",
"message.go-to-settings": "Mene asetuksiin", "message.go-to-settings": "Mene asetuksiin",
"message.incorrect-username-password": "Väärä käyttäjänimi/salasana.", "message.incorrect-username-password": "Väärä käyttäjänimi/salasana.",
"message.log.visitor": "Vierailija maasta {country} käyttäen selainta {browser} {os}-laitteella: {device}", "message.log.visitor": "Vierailija maasta {country} selaimella {browser} laitteella {os} {device}",
"message.new-version-available": "Uusi versio umamista {version} on käytettävissä!", "message.new-version-available": "Uusi versio umamista {version} on käytettävissä!",
"message.no-data-available": "Tietoja ei ole käytettävissä.", "message.no-data-available": "Tietoja ei ole käytettävissä.",
"message.no-websites-configured": "Sinulla ei ole määritettyjä verkkosivustoja.", "message.no-websites-configured": "Sinulla ei ole määritettyjä verkkosivustoja.",
"message.page-not-found": "Sivua ei löydetty.", "message.page-not-found": "Sivua ei löydetty.",
"message.powered-by": "Voimanlähteenä {name}", "message.powered-by": "Voimanlähteenä {name}",
"message.reset-warning": "All statistics for this website will be deleted, but your tracking code will remain intact.", "message.reset-warning": "Kaikki sivuston tilastot poistetaan, mutta seurantakoodi pysyy muuttumattomana.",
"message.save-success": "Tallennettu onnistuneesti.", "message.save-success": "Tallennettu onnistuneesti.",
"message.share-url": "Tämä on julkisesti jaettu URL-osoitteelle {target}.", "message.share-url": "Tämä on julkisesti jaettu URL sivustolle {target}.",
"message.toggle-charts": "Toggle charts", "message.toggle-charts": "Kytke kaaviot päälle/pois",
"message.track-stats": "Jos haluat seurata kohteen {target} tilastoja, aseta seuraava koodi verkkosivustosi {head} osioon.", "message.track-stats": "Jos haluat seurata sivuston {target} tilastoja, aseta seuraava koodi verkkosivustosi {head}-osioon.",
"message.type-delete": "Kirjoita {delete} alla olevaan ruutuun vahvistaaksesi.", "message.type-delete": "Kirjoita {delete} alla olevaan kenttään vahvistaaksesi.",
"message.type-reset": "Type {reset} in the box below to confirm.", "message.type-reset": "Kirjoita {reset} alla olevaan kenttään vahvistaaksesi.",
"metrics.actions": "Toiminnat", "metrics.actions": "Toiminnat",
"metrics.average-visit-time": "Keskimääräinen vierailuaika", "metrics.average-visit-time": "Keskimääräinen vierailuaika",
"metrics.bounce-rate": "Välitön poistuminen", "metrics.bounce-rate": "Välitön poistuminen",
@ -92,19 +92,19 @@
"metrics.countries": "Maat", "metrics.countries": "Maat",
"metrics.device.desktop": "Pöytäkone", "metrics.device.desktop": "Pöytäkone",
"metrics.device.laptop": "Kannettava tietokone", "metrics.device.laptop": "Kannettava tietokone",
"metrics.device.mobile": "Mobiili", "metrics.device.mobile": "Puhelin",
"metrics.device.tablet": "Tabletti", "metrics.device.tablet": "Tabletti",
"metrics.devices": "Laitteet", "metrics.devices": "Laitteet",
"metrics.events": "Tapahtumat", "metrics.events": "Tapahtumat",
"metrics.filter.combined": "Yhdistetty", "metrics.filter.combined": "Yhdistetty",
"metrics.filter.domain-only": "Vain verkkotunnus", "metrics.filter.domain-only": "Vain verkkotunnus",
"metrics.filter.raw": "Käsittelemätön", "metrics.filter.raw": "Käsittelemätön",
"metrics.languages": "Languages", "metrics.languages": "Kielet",
"metrics.operating-systems": "Käyttöjärjestelmät", "metrics.operating-systems": "Käyttöjärjestelmät",
"metrics.page-views": "Sivun näyttökertoja", "metrics.page-views": "Sivun näyttökerrat",
"metrics.pages": "Sivut", "metrics.pages": "Sivut",
"metrics.referrers": "Viittaajat", "metrics.referrers": "Viittaajat",
"metrics.unique-visitors": "Uniikit vierailijat", "metrics.unique-visitors": "Yksittäiset kävijät",
"metrics.views": "Näyttökertoja", "metrics.views": "Näyttökerrat",
"metrics.visitors": "Vierailijat" "metrics.visitors": "Vierailijat"
} }

View File

@ -84,7 +84,7 @@
"message.toggle-charts": "Toggle charts", "message.toggle-charts": "Toggle charts",
"message.track-stats": "Fyri at spora hagtøl fyri {target}, koyr kotuna í {head} partin á tínari heimasíðu.", "message.track-stats": "Fyri at spora hagtøl fyri {target}, koyr kotuna í {head} partin á tínari heimasíðu.",
"message.type-delete": "Skriva {delete} í feltið fyri at vátta", "message.type-delete": "Skriva {delete} í feltið fyri at vátta",
"message.type-reset": "Type {reset} in the box below to confirm.", "message.type-reset": "Skriva {reset} í feltið fyri at vátta",
"metrics.actions": "Gerðir", "metrics.actions": "Gerðir",
"metrics.average-visit-time": "Miðal vitjurnartíð ", "metrics.average-visit-time": "Miðal vitjurnartíð ",
"metrics.bounce-rate": "Bounce prosenttal", "metrics.bounce-rate": "Bounce prosenttal",

View File

@ -84,7 +84,7 @@
"message.toggle-charts": "Toggle charts", "message.toggle-charts": "Toggle charts",
"message.track-stats": "יש להוסיף את הקוד הבא לאזור ה-{head} של האתר", "message.track-stats": "יש להוסיף את הקוד הבא לאזור ה-{head} של האתר",
"message.type-delete": "הקלידו {delete} בתיבה על מנת לאשר", "message.type-delete": "הקלידו {delete} בתיבה על מנת לאשר",
"message.type-reset": "Type {reset} in the box below to confirm.", "message.type-reset": "הקלידו {reset} בתיבה על מנת לאשר",
"metrics.actions": "פעולות", "metrics.actions": "פעולות",
"metrics.average-visit-time": "זמן ביקור ממוצע", "metrics.average-visit-time": "זמן ביקור ממוצע",
"metrics.bounce-rate": "Bounce rate", "metrics.bounce-rate": "Bounce rate",

View File

@ -84,7 +84,7 @@
"message.toggle-charts": "Toggle charts", "message.toggle-charts": "Toggle charts",
"message.track-stats": "{target} के आँकड़ों को ट्रैक करने के लिए, अपनी वेबसाइट के {head} अनुभाग में निम्नलिखित कोड रखें।", "message.track-stats": "{target} के आँकड़ों को ट्रैक करने के लिए, अपनी वेबसाइट के {head} अनुभाग में निम्नलिखित कोड रखें।",
"message.type-delete": "पुष्टि करने के लिए नीचे दिए गए बॉक्स में {delete} टाइप करें।", "message.type-delete": "पुष्टि करने के लिए नीचे दिए गए बॉक्स में {delete} टाइप करें।",
"message.type-reset": "Type {reset} in the box below to confirm.", "message.type-reset": "ुष्टि करने के लिए नीचे दिए गए बॉक्स में {reset} टाइप करें।",
"metrics.actions": "कार्य", "metrics.actions": "कार्य",
"metrics.average-visit-time": "औसत दृश्य समय", "metrics.average-visit-time": "औसत दृश्य समय",
"metrics.bounce-rate": "उछाल दर", "metrics.bounce-rate": "उछाल दर",

View File

@ -84,7 +84,7 @@
"message.toggle-charts": "Toggle charts", "message.toggle-charts": "Toggle charts",
"message.track-stats": "{target} statisztikáinak nyomon követéséhez, helyezd el az alábbi kódot a weboldalad {head} részébe.", "message.track-stats": "{target} statisztikáinak nyomon követéséhez, helyezd el az alábbi kódot a weboldalad {head} részébe.",
"message.type-delete": "Megerősítéshez írd be az alábbi mezőbe azt, hogy {delete}.", "message.type-delete": "Megerősítéshez írd be az alábbi mezőbe azt, hogy {delete}.",
"message.type-reset": "Type {reset} in the box below to confirm.", "message.type-reset": "Megerősítéshez írd be az alábbi mezőbe azt, hogy {reset}.",
"metrics.actions": "Műveletek", "metrics.actions": "Műveletek",
"metrics.average-visit-time": "Átlagos látogatási idő", "metrics.average-visit-time": "Átlagos látogatási idő",
"metrics.bounce-rate": "Visszafordulási arány", "metrics.bounce-rate": "Visszafordulási arány",

View File

@ -84,7 +84,7 @@
"message.toggle-charts": "Toggle charts", "message.toggle-charts": "Toggle charts",
"message.track-stats": "Untuk melacak statistik {target}, tempatkan kode berikut di bagian {head} situs web anda.", "message.track-stats": "Untuk melacak statistik {target}, tempatkan kode berikut di bagian {head} situs web anda.",
"message.type-delete": "Ketikkan {delete} pada kotak di bawah untuk konfirmasi.", "message.type-delete": "Ketikkan {delete} pada kotak di bawah untuk konfirmasi.",
"message.type-reset": "Type {reset} in the box below to confirm.", "message.type-reset": "Ketikkan {reset} pada kotak di bawah untuk konfirmasi.",
"metrics.actions": "Aksi", "metrics.actions": "Aksi",
"metrics.average-visit-time": "Waktu kunjungan rata-rata", "metrics.average-visit-time": "Waktu kunjungan rata-rata",
"metrics.bounce-rate": "Rasio pentalan", "metrics.bounce-rate": "Rasio pentalan",

View File

@ -84,7 +84,7 @@
"message.toggle-charts": "Toggle charts", "message.toggle-charts": "Toggle charts",
"message.track-stats": "{target}のアクセス解析を開始するには、次のコードをWebサイトの{head}セクションへ追加してください。", "message.track-stats": "{target}のアクセス解析を開始するには、次のコードをWebサイトの{head}セクションへ追加してください。",
"message.type-delete": "確認のため、下のフォームに{delete}と入力してください。", "message.type-delete": "確認のため、下のフォームに{delete}と入力してください。",
"message.type-reset": "Type {reset} in the box below to confirm.", "message.type-reset": "確認のため、下のフォームに{reset}と入力してください。",
"metrics.actions": "アクション", "metrics.actions": "アクション",
"metrics.average-visit-time": "平均滞在時間", "metrics.average-visit-time": "平均滞在時間",
"metrics.bounce-rate": "直帰率", "metrics.bounce-rate": "直帰率",

View File

@ -84,7 +84,7 @@
"message.toggle-charts": "Toggle charts", "message.toggle-charts": "Toggle charts",
"message.track-stats": "{target}에 대한 통계를 추적하려면 웹사이트의 {head} 섹션에 다음 코드를 입력하십시오.", "message.track-stats": "{target}에 대한 통계를 추적하려면 웹사이트의 {head} 섹션에 다음 코드를 입력하십시오.",
"message.type-delete": "확인을 위해 아래 박스에 {delete}값을 입력하십시오.", "message.type-delete": "확인을 위해 아래 박스에 {delete}값을 입력하십시오.",
"message.type-reset": "Type {reset} in the box below to confirm.", "message.type-reset": "확인을 위해 아래 박스에 {reset}값을 입력하십시오.",
"metrics.actions": "액션", "metrics.actions": "액션",
"metrics.average-visit-time": "평균 방문 시간", "metrics.average-visit-time": "평균 방문 시간",
"metrics.bounce-rate": "이탈률", "metrics.bounce-rate": "이탈률",

View File

@ -84,7 +84,7 @@
"message.toggle-charts": "Toggle charts", "message.toggle-charts": "Toggle charts",
"message.track-stats": "{target} вебийн статистикийг бүртгэхийн тулд доорх кодыг вебийнхээ {head} хэсэгт байрлуулна уу.", "message.track-stats": "{target} вебийн статистикийг бүртгэхийн тулд доорх кодыг вебийнхээ {head} хэсэгт байрлуулна уу.",
"message.type-delete": "Доорх хэсэгт {delete} гэж бичиж баталгаажуулна уу.", "message.type-delete": "Доорх хэсэгт {delete} гэж бичиж баталгаажуулна уу.",
"message.type-reset": "Type {reset} in the box below to confirm.", "message.type-reset": "Доорх хэсэгт {reset} гэж бичиж баталгаажуулна уу.",
"metrics.actions": "Үйлдлүүд", "metrics.actions": "Үйлдлүүд",
"metrics.average-visit-time": "Зочилсон дундаж хугацаа", "metrics.average-visit-time": "Зочилсон дундаж хугацаа",
"metrics.bounce-rate": "Нэг хуудас үзээд гарсан", "metrics.bounce-rate": "Нэг хуудас үзээд гарсан",

View File

@ -84,7 +84,7 @@
"message.toggle-charts": "Toggle charts", "message.toggle-charts": "Toggle charts",
"message.track-stats": "Untuk menjejak statistik bagi {target}, letakkan kod berikut di bahagian {head} laman web anda.", "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.", "message.type-delete": "Taip {delete} di dalam kotak di bawah untuk pengesahan.",
"message.type-reset": "Type {reset} in the box below to confirm.", "message.type-reset": "Taip {reset} di dalam kotak di bawah untuk pengesahan.",
"metrics.actions": "Aksi", "metrics.actions": "Aksi",
"metrics.average-visit-time": "Purata tempoh masa lawatan", "metrics.average-visit-time": "Purata tempoh masa lawatan",
"metrics.bounce-rate": "Kadar lantunan", "metrics.bounce-rate": "Kadar lantunan",

View File

@ -84,7 +84,7 @@
"message.toggle-charts": "Toggle charts", "message.toggle-charts": "Toggle charts",
"message.track-stats": "For å spore statistikk for {target}, plasser følgende kode i {head}-delen av nettstedet ditt.", "message.track-stats": "For å spore statistikk for {target}, plasser følgende kode i {head}-delen av nettstedet ditt.",
"message.type-delete": "Skriv inn {delete} i boksen nedenfor for å bekrefte.", "message.type-delete": "Skriv inn {delete} i boksen nedenfor for å bekrefte.",
"message.type-reset": "Type {reset} in the box below to confirm.", "message.type-reset": "Skriv inn {reset} i boksen nedenfor for å bekrefte.",
"metrics.actions": "Handlinger", "metrics.actions": "Handlinger",
"metrics.average-visit-time": "Gjennomsnittlig besøkelsestid", "metrics.average-visit-time": "Gjennomsnittlig besøkelsestid",
"metrics.bounce-rate": "Avvisningsfrekvens", "metrics.bounce-rate": "Avvisningsfrekvens",

View File

@ -84,7 +84,7 @@
"message.toggle-charts": "Toggle charts", "message.toggle-charts": "Toggle charts",
"message.track-stats": "Om statistieken voor {target} bij te houden, plaats je de volgende code in het {head} gedeelte van je website.", "message.track-stats": "Om statistieken voor {target} bij te houden, plaats je de volgende code in het {head} gedeelte van je website.",
"message.type-delete": "Type {delete} in onderstaande veld om dit te bevestigen.", "message.type-delete": "Type {delete} in onderstaande veld om dit te bevestigen.",
"message.type-reset": "Type {reset} in the box below to confirm.", "message.type-reset": "Type {reset} in onderstaande veld om dit te bevestigen.",
"metrics.actions": "Acties", "metrics.actions": "Acties",
"metrics.average-visit-time": "Gemiddelde bezoektijd", "metrics.average-visit-time": "Gemiddelde bezoektijd",
"metrics.bounce-rate": "Bouncepercentage", "metrics.bounce-rate": "Bouncepercentage",

View File

@ -84,7 +84,7 @@
"message.toggle-charts": "Toggle charts", "message.toggle-charts": "Toggle charts",
"message.track-stats": "Aby śledzić statystyki dla {target}, umieść poniższy kod w sekcji {head} swojej witryny.", "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ć.", "message.type-delete": "Wpisz {delete} w polu poniżej, aby potwierdzić.",
"message.type-reset": "Type {reset} in the box below to confirm.", "message.type-reset": "Wpisz {reset} w polu poniżej, aby potwierdzić.",
"metrics.actions": "Działania", "metrics.actions": "Działania",
"metrics.average-visit-time": "Średni czas wizyty", "metrics.average-visit-time": "Średni czas wizyty",
"metrics.bounce-rate": "Współczynnik odrzuceń", "metrics.bounce-rate": "Współczynnik odrzuceń",

View File

@ -84,7 +84,7 @@
"message.toggle-charts": "Toggle charts", "message.toggle-charts": "Toggle charts",
"message.track-stats": "Para gerar estatística para {target}, coloque o seguinte código no {head} do html do seu site.", "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.", "message.type-delete": "Escreva {delete} abaixo para continuar.",
"message.type-reset": "Type {reset} in the box below to confirm.", "message.type-reset": "Escreva {reset} abaixo para continuar.",
"metrics.actions": "Ações", "metrics.actions": "Ações",
"metrics.average-visit-time": "Tempo médio da visita", "metrics.average-visit-time": "Tempo médio da visita",
"metrics.bounce-rate": "Taxa de rejeição", "metrics.bounce-rate": "Taxa de rejeição",

View File

@ -84,7 +84,7 @@
"message.toggle-charts": "Toggle charts", "message.toggle-charts": "Toggle charts",
"message.track-stats": "Pre sledovanie návštev na {target}, pridajte následujúci kód do {head} časti vašeho webu.", "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.", "message.type-delete": "Napíšte {delete} pre potvrdenie.",
"message.type-reset": "Type {reset} in the box below to confirm.", "message.type-reset": "Napíšte {reset} pre potvrdenie.",
"metrics.actions": "Akcie", "metrics.actions": "Akcie",
"metrics.average-visit-time": "Priemerný čas návštevy", "metrics.average-visit-time": "Priemerný čas návštevy",
"metrics.bounce-rate": "Okamžité opustenie", "metrics.bounce-rate": "Okamžité opustenie",

View File

@ -84,7 +84,7 @@
"message.toggle-charts": "Toggle charts", "message.toggle-charts": "Toggle charts",
"message.track-stats": "Če želite spremljati statistične podatke za {target}, v {head} del vašega spletnega mesta namestite naslednjo kodo.", "message.track-stats": "Če želite spremljati statistične podatke za {target}, v {head} del vašega spletnega mesta namestite naslednjo kodo.",
"message.type-delete": "V spodnje polje vnesite {delete} za potrditev.", "message.type-delete": "V spodnje polje vnesite {delete} za potrditev.",
"message.type-reset": "Type {reset} in the box below to confirm.", "message.type-reset": "V spodnje polje vnesite {reset} za potrditev.",
"metrics.actions": "Dejanja", "metrics.actions": "Dejanja",
"metrics.average-visit-time": "Povprečni čas obiska", "metrics.average-visit-time": "Povprečni čas obiska",
"metrics.bounce-rate": "Zapustna stopnja", "metrics.bounce-rate": "Zapustna stopnja",

View File

@ -4,8 +4,8 @@
"label.add-website": "Lägg till webbsajt", "label.add-website": "Lägg till webbsajt",
"label.administrator": "Administratör", "label.administrator": "Administratör",
"label.all": "Alla", "label.all": "Alla",
"label.all-events": "All events", "label.all-events": "Alla händelser",
"label.all-time": "All time", "label.all-time": "Sedan början",
"label.all-websites": "Alla sajter", "label.all-websites": "Alla sajter",
"label.back": "Tillbaka", "label.back": "Tillbaka",
"label.cancel": "Avbryt", "label.cancel": "Avbryt",
@ -28,7 +28,7 @@
"label.enable-share-url": "Aktivera delnings-URL", "label.enable-share-url": "Aktivera delnings-URL",
"label.invalid": "Ogiltig", "label.invalid": "Ogiltig",
"label.invalid-domain": "Ogiltig domän", "label.invalid-domain": "Ogiltig domän",
"label.language": "Language", "label.language": "Språk",
"label.last-days": "Senaste {x} dagarna", "label.last-days": "Senaste {x} dagarna",
"label.last-hours": "Senaste {x} timmarna", "label.last-hours": "Senaste {x} timmarna",
"label.logged-in-as": "Inloggad som {username}", "label.logged-in-as": "Inloggad som {username}",
@ -37,7 +37,7 @@
"label.more": "Mer", "label.more": "Mer",
"label.name": "Namn", "label.name": "Namn",
"label.new-password": "Nytt lösenord", "label.new-password": "Nytt lösenord",
"label.owner": "Owner", "label.owner": "Ägare",
"label.password": "Lösenord", "label.password": "Lösenord",
"label.passwords-dont-match": "Lösenorden är inte samma", "label.passwords-dont-match": "Lösenorden är inte samma",
"label.profile": "Profil", "label.profile": "Profil",
@ -46,12 +46,12 @@
"label.refresh": "Uppdatera", "label.refresh": "Uppdatera",
"label.required": "Krävs", "label.required": "Krävs",
"label.reset": "Återställ", "label.reset": "Återställ",
"label.reset-website": "Reset statistics", "label.reset-website": "Återställ statistik",
"label.save": "Spara", "label.save": "Spara",
"label.settings": "Inställningar", "label.settings": "Inställningar",
"label.share-url": "Delnings-URL", "label.share-url": "Delnings-URL",
"label.single-day": "En dag", "label.single-day": "En dag",
"label.theme": "Theme", "label.theme": "Tema",
"label.this-month": "Denna månad", "label.this-month": "Denna månad",
"label.this-week": "Denna vecka", "label.this-week": "Denna vecka",
"label.this-year": "Detta år", "label.this-year": "Detta år",
@ -64,27 +64,27 @@
"label.websites": "Webbsajt", "label.websites": "Webbsajt",
"message.active-users": "{x} {x, plural, one {besökare} other {besökare}} just nu", "message.active-users": "{x} {x, plural, one {besökare} other {besökare}} just nu",
"message.confirm-delete": "Är du säker på att du vill radera {target}?", "message.confirm-delete": "Är du säker på att du vill radera {target}?",
"message.confirm-reset": "Are your sure you want to reset {target}'s statistics?", "message.confirm-reset": "Är du säker på att du vill återställa statistiken för {target}?",
"message.copied": "Kopierad!", "message.copied": "Kopierad!",
"message.delete-warning": "All tillhörande data kommer också raderas.", "message.delete-warning": "All tillhörande data kommer också raderas.",
"message.failure": "Något gick fel.", "message.failure": "Något gick fel.",
"message.get-share-url": "Visa delnings-URL", "message.get-share-url": "Visa delnings-URL",
"message.get-tracking-code": "Visa spårningskod", "message.get-tracking-code": "Visa spårningskod",
"message.go-to-settings": "Gå till inställningar", "message.go-to-settings": "Gå till inställningar",
"message.incorrect-username-password": "Felaktikt användarnamn/lösenord.", "message.incorrect-username-password": "Felaktigt användarnamn/lösenord.",
"message.log.visitor": "Besökare från {country} med {browser} på {os} {device}", "message.log.visitor": "Besökare från {country} med {browser} på {os} {device}",
"message.new-version-available": "En ny version av umami {version} är tillgänglig!", "message.new-version-available": "En ny version av umami {version} är tillgänglig!",
"message.no-data-available": "Ingen data tillgänglig.", "message.no-data-available": "Ingen data tillgänglig.",
"message.no-websites-configured": "Du har inga webbsajter.", "message.no-websites-configured": "Du har inga webbsajter.",
"message.page-not-found": "Sidan kan inte hittas.", "message.page-not-found": "Sidan kan inte hittas.",
"message.powered-by": "Drivs av {name}", "message.powered-by": "Drivs av {name}",
"message.reset-warning": "All statistics for this website will be deleted, but your tracking code will remain intact.", "message.reset-warning": "All statistik för webbsajten tas bort men spårningskoden förblir oförändrad.",
"message.save-success": "Sparades!", "message.save-success": "Sparades!",
"message.share-url": "Det här är den offentliga delnings-URL:en {target}.", "message.share-url": "Det här är den offentliga delnings-URL:en för {target}.",
"message.toggle-charts": "Toggle charts", "message.toggle-charts": "Visa/göm grafer",
"message.track-stats": "För att spåra statistik för {target}, placera följande kod i {head}-avsnittet på din webbsajt.", "message.track-stats": "För att spåra statistik för {target}, placera följande kod i {head}-avsnittet på din webbsajt.",
"message.type-delete": "Skriv {delete} i rutan nedan för att radera.", "message.type-delete": "Skriv {delete} i rutan nedan för att radera.",
"message.type-reset": "Type {reset} in the box below to confirm.", "message.type-reset": "Skriv {reset} i rutan nedan för att bekräfta.",
"metrics.actions": "Händelser", "metrics.actions": "Händelser",
"metrics.average-visit-time": "Medelbesökstid", "metrics.average-visit-time": "Medelbesökstid",
"metrics.bounce-rate": "Avvisningfrekvens", "metrics.bounce-rate": "Avvisningfrekvens",
@ -99,7 +99,7 @@
"metrics.filter.combined": "Kombinerade", "metrics.filter.combined": "Kombinerade",
"metrics.filter.domain-only": "Endast domän", "metrics.filter.domain-only": "Endast domän",
"metrics.filter.raw": "Rådata", "metrics.filter.raw": "Rådata",
"metrics.languages": "Languages", "metrics.languages": "Språk",
"metrics.operating-systems": "Operativsystem", "metrics.operating-systems": "Operativsystem",
"metrics.page-views": "Sidvisningar", "metrics.page-views": "Sidvisningar",
"metrics.pages": "Sidor", "metrics.pages": "Sidor",

View File

@ -84,7 +84,7 @@
"message.toggle-charts": "Toggle charts", "message.toggle-charts": "Toggle charts",
"message.track-stats": "{target}க்கான புள்ளிவிவரங்களைக் கண்காணிக்க, {head}ல் பின்வரும் குறியீட்டை வைக்கவும்.", "message.track-stats": "{target}க்கான புள்ளிவிவரங்களைக் கண்காணிக்க, {head}ல் பின்வரும் குறியீட்டை வைக்கவும்.",
"message.type-delete": "உறுதிப்படுத்த கீழே உள்ள பெட்டியில் {delete} என தட்டச்சு செய்க.", "message.type-delete": "உறுதிப்படுத்த கீழே உள்ள பெட்டியில் {delete} என தட்டச்சு செய்க.",
"message.type-reset": "Type {reset} in the box below to confirm.", "message.type-reset": "உறுதிப்படுத்த கீழே உள்ள பெட்டியில் {reset} என தட்டச்சு செய்க.",
"metrics.actions": "செயல்கள்", "metrics.actions": "செயல்கள்",
"metrics.average-visit-time": "சராசரி வருகை நேரம்", "metrics.average-visit-time": "சராசரி வருகை நேரம்",
"metrics.bounce-rate": "துள்ளல் விகிதம்", "metrics.bounce-rate": "துள்ளல் விகிதம்",

View File

@ -84,7 +84,7 @@
"message.toggle-charts": "Toggle charts", "message.toggle-charts": "Toggle charts",
"message.track-stats": "{target} alanı adı istatistiklerini takip etmek için, aşağıdaki kodu web sitenizin {head} bloğuna yerleştirin.", "message.track-stats": "{target} alanı adı istatistiklerini takip etmek için, aşağıdaki kodu web sitenizin {head} bloğuna yerleştirin.",
"message.type-delete": "Onaylamak için kutuya {delete} yazın.", "message.type-delete": "Onaylamak için kutuya {delete} yazın.",
"message.type-reset": "Type {reset} in the box below to confirm.", "message.type-reset": "Onaylamak için kutuya {reset} yazın.",
"metrics.actions": "Hareketler", "metrics.actions": "Hareketler",
"metrics.average-visit-time": "Ortalama ziyaret süresi", "metrics.average-visit-time": "Ortalama ziyaret süresi",
"metrics.bounce-rate": ıkma oranı", "metrics.bounce-rate": ıkma oranı",

View File

@ -84,7 +84,7 @@
"message.toggle-charts": "Toggle charts", "message.toggle-charts": "Toggle charts",
"message.track-stats": "Để theo dõi {target}, dán mã theo dõi vào {head} của website bạn.", "message.track-stats": "Để theo dõi {target}, dán mã theo dõi vào {head} của website bạn.",
"message.type-delete": "Nhập {delete} bên dưới để xác nhận.", "message.type-delete": "Nhập {delete} bên dưới để xác nhận.",
"message.type-reset": "Type {reset} in the box below to confirm.", "message.type-reset": "Nhập {reset} bên dưới để xác nhận.",
"metrics.actions": "Hành động", "metrics.actions": "Hành động",
"metrics.average-visit-time": "Thời gian truy cập trung bình", "metrics.average-visit-time": "Thời gian truy cập trung bình",
"metrics.bounce-rate": "Tỷ lệ thoát trang", "metrics.bounce-rate": "Tỷ lệ thoát trang",

View File

@ -84,7 +84,7 @@
"message.toggle-charts": "Toggle charts", "message.toggle-charts": "Toggle charts",
"message.track-stats": "把以下代码放到你的网站的 {head} 部分来收集 {target} 的数据。", "message.track-stats": "把以下代码放到你的网站的 {head} 部分来收集 {target} 的数据。",
"message.type-delete": "在下方输入框输入 {delete} 以确认删除。", "message.type-delete": "在下方输入框输入 {delete} 以确认删除。",
"message.type-reset": "Type {reset} in the box below to confirm.", "message.type-reset": "在下方输入框输入 {reset} 以确认删除。",
"metrics.actions": "用户行为", "metrics.actions": "用户行为",
"metrics.average-visit-time": "平均访问时间", "metrics.average-visit-time": "平均访问时间",
"metrics.bounce-rate": "跳出率", "metrics.bounce-rate": "跳出率",

View File

@ -84,7 +84,7 @@
"message.toggle-charts": "Toggle charts", "message.toggle-charts": "Toggle charts",
"message.track-stats": "將以下代碼放入被設定網站的 {head} 部分來收集 {target} 的資料。", "message.track-stats": "將以下代碼放入被設定網站的 {head} 部分來收集 {target} 的資料。",
"message.type-delete": "在下方空格輸入 {delete} 確認", "message.type-delete": "在下方空格輸入 {delete} 確認",
"message.type-reset": "Type {reset} in the box below to confirm.", "message.type-reset": "在下方空格輸入 {reset} 確認",
"metrics.actions": "用戶行為", "metrics.actions": "用戶行為",
"metrics.average-visit-time": "平均訪問時間", "metrics.average-visit-time": "平均訪問時間",
"metrics.bounce-rate": "跳出率", "metrics.bounce-rate": "跳出率",

View File

@ -1,5 +1,5 @@
import { BROWSERS } from './constants'; import { BROWSERS } from './constants';
import { removeTrailingSlash, removeWWW, getDomainName } from './url'; import { removeTrailingSlash, removeWWW } from './url';
export const urlFilter = (data, { raw }) => { export const urlFilter = (data, { raw }) => {
const isValidUrl = url => { const isValidUrl = url => {
@ -46,23 +46,18 @@ export const urlFilter = (data, { raw }) => {
}; };
export const refFilter = (data, { domain, domainOnly, raw }) => { export const refFilter = (data, { domain, domainOnly, raw }) => {
const domainName = getDomainName(domain); const regex = new RegExp(`http[s]?://([a-z0-9-]+\\.)*${domain}`);
const regex = new RegExp(`http[s]?://${domainName}`);
const links = {}; const links = {};
const isValidRef = ref => { const isValidRef = ref => {
return ref !== '' && ref !== null && !ref.startsWith('/') && !ref.startsWith('#'); return ref !== '' && ref !== null && !ref.startsWith('/') && !ref.startsWith('#');
}; };
if (raw) {
return data.filter(({ x }) => isValidRef(x) && !regex.test(x));
}
const cleanUrl = url => { const cleanUrl = url => {
try { try {
const { hostname, origin, pathname, searchParams, protocol } = new URL(url); const { hostname, origin, pathname, searchParams, protocol } = new URL(url);
if (hostname === domainName) { if (regex.test(url)) {
return null; return null;
} }
@ -88,6 +83,10 @@ export const refFilter = (data, { domain, domainOnly, raw }) => {
} }
}; };
if (raw) {
return data.filter(({ x }) => isValidRef(x) && !regex.test(x));
}
const map = data.reduce((obj, { x, y }) => { const map = data.reduce((obj, { x, y }) => {
if (!isValidRef(x)) { if (!isValidRef(x)) {
return obj; return obj;

View File

@ -86,3 +86,11 @@ export async function getClientInfo(req, { screen }) {
return { userAgent, browser, os, ip, country, device }; return { userAgent, browser, os, ip, country, device };
} }
export function getJsonBody(req) {
if ((req.headers['content-type'] || '').indexOf('text/plain') !== -1) {
return JSON.parse(req.body);
}
return req.body;
}

View File

@ -1,7 +1,15 @@
export function ok(res, data = {}) { export function ok(res, data = {}) {
return json(res, data);
}
export function json(res, data = {}) {
return res.status(200).json(data); return res.status(200).json(data);
} }
export function send(res, data) {
return res.status(200).send(data);
}
export function redirect(res, url) { export function redirect(res, url) {
res.setHeader('Location', url); res.setHeader('Location', url);

View File

@ -1,9 +1,9 @@
import { getWebsiteByUuid, getSessionByUuid, createSession } from 'lib/queries'; import { getWebsiteByUuid, getSessionByUuid, createSession } from 'lib/queries';
import { getClientInfo } from 'lib/request'; import { getJsonBody, getClientInfo } from 'lib/request';
import { uuid, isValidUuid, parseToken } from 'lib/crypto'; import { uuid, isValidUuid, parseToken } from 'lib/crypto';
export async function getSession(req) { export async function getSession(req) {
const { payload } = req.body; const { payload } = getJsonBody(req);
if (!payload) { if (!payload) {
throw new Error('Invalid request'); throw new Error('Invalid request');
@ -32,7 +32,7 @@ export async function getSession(req) {
} }
const { website_id } = website; const { website_id } = website;
const session_uuid = uuid(website_id, hostname, ip, userAgent, os); const session_uuid = uuid(website_id, hostname, ip, userAgent);
let session = await getSessionByUuid(session_uuid); let session = await getSessionByUuid(session_uuid);

View File

@ -1,6 +1,6 @@
{ {
"name": "umami", "name": "umami",
"version": "1.27.0", "version": "1.28.0",
"description": "A simple, fast, website analytics alternative to Google Analytics.", "description": "A simple, fast, website analytics alternative to Google Analytics.",
"author": "Mike Cao <mike@mikecao.com>", "author": "Mike Cao <mike@mikecao.com>",
"license": "MIT", "license": "MIT",
@ -21,10 +21,11 @@
"build-geo": "node scripts/build-geo.js", "build-geo": "node scripts/build-geo.js",
"build-db-schema": "dotenv prisma introspect", "build-db-schema": "dotenv prisma introspect",
"build-db-client": "dotenv prisma generate", "build-db-client": "dotenv prisma generate",
"build-mysql-schema": "dotenv prisma introspect -- --schema=./prisma/schema.mysql.prisma", "build-mysql-schema": "dotenv prisma db pull -- --schema=./prisma/schema.mysql.prisma",
"build-mysql-client": "dotenv prisma generate -- --schema=./prisma/schema.mysql.prisma", "build-mysql-client": "dotenv prisma generate -- --schema=./prisma/schema.mysql.prisma",
"build-postgresql-schema": "dotenv prisma introspect -- --schema=./prisma/schema.postgresql.prisma", "build-postgresql-schema": "dotenv prisma db pull -- --schema=./prisma/schema.postgresql.prisma",
"build-postgresql-client": "dotenv prisma generate -- --schema=./prisma/schema.postgresql.prisma", "build-postgresql-client": "dotenv prisma generate -- --schema=./prisma/schema.postgresql.prisma",
"postbuild": "node scripts/postbuild.js",
"copy-db-schema": "node scripts/copy-db-schema.js", "copy-db-schema": "node scripts/copy-db-schema.js",
"generate-lang": "npm-run-all extract-lang merge-lang", "generate-lang": "npm-run-all extract-lang merge-lang",
"extract-lang": "formatjs extract \"{pages,components}/**/*.js\" --out-file build/messages.json", "extract-lang": "formatjs extract \"{pages,components}/**/*.js\" --out-file build/messages.json",
@ -52,8 +53,9 @@
] ]
}, },
"dependencies": { "dependencies": {
"@fontsource/inter": "4.5.4", "@fontsource/inter": "4.5.5",
"@prisma/client": "3.9.2", "@prisma/client": "3.11.0",
"async-retry": "^1.3.3",
"bcryptjs": "^2.4.3", "bcryptjs": "^2.4.3",
"chalk": "^4.1.1", "chalk": "^4.1.1",
"chart.js": "^2.9.4", "chart.js": "^2.9.4",
@ -65,21 +67,25 @@
"detect-browser": "^5.2.0", "detect-browser": "^5.2.0",
"dotenv": "^10.0.0", "dotenv": "^10.0.0",
"formik": "^2.2.9", "formik": "^2.2.9",
"fs-extra": "^10.0.1",
"immer": "^9.0.12", "immer": "^9.0.12",
"ipaddr.js": "^2.0.1", "ipaddr.js": "^2.0.1",
"is-ci": "^3.0.1",
"is-docker": "^3.0.0",
"is-localhost-ip": "^1.4.0", "is-localhost-ip": "^1.4.0",
"isbot": "^3.2.2", "isbot": "^3.4.5",
"jose": "2.0.5", "jose": "2.0.5",
"maxmind": "^4.3.2", "maxmind": "^4.3.6",
"moment-timezone": "^0.5.33", "moment-timezone": "^0.5.33",
"next": "12.1.0", "next": "12.1.0",
"node-fetch": "^3.2.3",
"prompts": "2.4.2", "prompts": "2.4.2",
"prop-types": "^15.7.2", "prop-types": "^15.7.2",
"react": "^17.0.2", "react": "^17.0.2",
"react-dom": "^17.0.2", "react-dom": "^17.0.2",
"react-intl": "^5.20.6", "react-intl": "^5.24.7",
"react-simple-maps": "^2.3.0", "react-simple-maps": "^2.3.0",
"react-spring": "^9.4.3", "react-spring": "^9.4.4",
"react-tooltip": "^4.2.21", "react-tooltip": "^4.2.21",
"react-use-measure": "^2.0.4", "react-use-measure": "^2.0.4",
"react-window": "^1.8.6", "react-window": "^1.8.6",
@ -101,21 +107,21 @@
"eslint-config-next": "^12.0.1", "eslint-config-next": "^12.0.1",
"eslint-config-prettier": "^8.3.0", "eslint-config-prettier": "^8.3.0",
"eslint-plugin-prettier": "^4.0.0", "eslint-plugin-prettier": "^4.0.0",
"eslint-plugin-react": "^7.24.0", "eslint-plugin-react": "^7.29.4",
"eslint-plugin-react-hooks": "^4.2.0", "eslint-plugin-react-hooks": "^4.2.0",
"extract-react-intl-messages": "^4.1.1", "extract-react-intl-messages": "^4.1.1",
"husky": "^7.0.0", "husky": "^7.0.0",
"lint-staged": "^11.0.0", "lint-staged": "^11.0.0",
"npm-run-all": "^4.1.5", "npm-run-all": "^4.1.5",
"postcss": "^8.2.15", "postcss": "^8.4.12",
"postcss-flexbugs-fixes": "^5.0.2", "postcss-flexbugs-fixes": "^5.0.2",
"postcss-import": "^14.0.2", "postcss-import": "^14.0.2",
"postcss-preset-env": "^7.4.2", "postcss-preset-env": "^7.4.2",
"postcss-rtlcss": "^3.3.2", "postcss-rtlcss": "^3.5.3",
"prettier": "^2.3.2", "prettier": "^2.6.0",
"prettier-eslint": "^13.0.0", "prettier-eslint": "^13.0.0",
"prisma": "3.9.2", "prisma": "3.11.0",
"rollup": "^2.69.0", "rollup": "^2.70.1",
"rollup-plugin-terser": "^7.0.2", "rollup-plugin-terser": "^7.0.2",
"stylelint": "^14.5.3", "stylelint": "^14.5.3",
"stylelint-config-css-modules": "^3.0.0", "stylelint-config-css-modules": "^3.0.0",

View File

@ -1,12 +1,13 @@
import { NextResponse } from 'next/server'; import { NextResponse } from 'next/server';
function redirectHTTPS(req) { function redirectHTTPS(req) {
const host = req.headers.get('host');
if ( if (
process.env.FORCE_SSL && process.env.FORCE_SSL &&
!req.headers.get('host').includes('localhost') && process.env.NODE_ENV === 'production' &&
req.nextUrl.protocol !== 'https' req.nextUrl.protocol === 'http:'
) { ) {
return NextResponse.redirect(`https://${req.headers.get('host')}${req.nextUrl.pathname}`, 301); return NextResponse.redirect(`https://${host}${req.nextUrl.pathname}`, 301);
} }
} }

View File

@ -2,8 +2,8 @@ import isbot from 'isbot';
import ipaddr from 'ipaddr.js'; import ipaddr from 'ipaddr.js';
import { savePageView, saveEvent } from 'lib/queries'; import { savePageView, saveEvent } from 'lib/queries';
import { useCors, useSession } from 'lib/middleware'; import { useCors, useSession } from 'lib/middleware';
import { getIpAddress } from 'lib/request'; import { getJsonBody, getIpAddress } from 'lib/request';
import { ok, badRequest } from 'lib/response'; import { ok, send, badRequest } from 'lib/response';
import { createToken } from 'lib/crypto'; import { createToken } from 'lib/crypto';
import { removeTrailingSlash } from 'lib/url'; import { removeTrailingSlash } from 'lib/url';
@ -39,10 +39,11 @@ export default async (req, res) => {
await useSession(req, res); await useSession(req, res);
const { const {
body: { type, payload },
session: { website_id, session_id }, session: { website_id, session_id },
} = req; } = req;
const { type, payload } = getJsonBody(req);
let { url, referrer, event_type, event_value } = payload; let { url, referrer, event_type, event_value } = payload;
if (process.env.REMOVE_TRAILING_SLASH) { if (process.env.REMOVE_TRAILING_SLASH) {
@ -59,5 +60,5 @@ export default async (req, res) => {
const token = await createToken({ website_id, session_id }); const token = await createToken({ website_id, session_id });
return ok(res, token); return send(res, token);
}; };

View File

@ -1,14 +1,10 @@
import React from 'react'; import React from 'react';
import { useRouter } from 'next/router';
import Layout from 'components/layout/Layout'; import Layout from 'components/layout/Layout';
import WebsiteList from 'components/pages/WebsiteList'; import Dashboard from 'components/pages/Dashboard';
import useRequireLogin from 'hooks/useRequireLogin'; import useRequireLogin from 'hooks/useRequireLogin';
export default function DashboardPage() { export default function DashboardPage() {
const { loading } = useRequireLogin(); const { loading } = useRequireLogin();
const router = useRouter();
const { id } = router.query;
const userId = id?.[0];
if (loading) { if (loading) {
return null; return null;
@ -16,7 +12,7 @@ export default function DashboardPage() {
return ( return (
<Layout> <Layout>
<WebsiteList userId={userId} /> <Dashboard />
</Layout> </Layout>
); );
} }

View File

@ -648,7 +648,7 @@
"message.type-reset": [ "message.type-reset": [
{ {
"type": 0, "type": 0,
"value": "Type " "value": "اكتب "
}, },
{ {
"type": 1, "type": 1,
@ -656,7 +656,7 @@
}, },
{ {
"type": 0, "type": 0,
"value": " in the box below to confirm." "value": " في الحقل التالي لتأكيد الحذف."
} }
], ],
"metrics.actions": [ "metrics.actions": [

View File

@ -648,7 +648,7 @@
"message.type-reset": [ "message.type-reset": [
{ {
"type": 0, "type": 0,
"value": "Type " "value": "Napište "
}, },
{ {
"type": 1, "type": 1,
@ -656,7 +656,7 @@
}, },
{ {
"type": 0, "type": 0,
"value": " in the box below to confirm." "value": " pro potvrzení."
} }
], ],
"metrics.actions": [ "metrics.actions": [

View File

@ -648,7 +648,7 @@
"message.type-reset": [ "message.type-reset": [
{ {
"type": 0, "type": 0,
"value": "Type " "value": "Skriv "
}, },
{ {
"type": 1, "type": 1,
@ -656,7 +656,7 @@
}, },
{ {
"type": 0, "type": 0,
"value": " in the box below to confirm." "value": " i boksen nedenfor, for at bekræfte."
} }
], ],
"metrics.actions": [ "metrics.actions": [

View File

@ -648,7 +648,7 @@
"message.type-reset": [ "message.type-reset": [
{ {
"type": 0, "type": 0,
"value": "Type " "value": "Πληκτρολογήστε "
}, },
{ {
"type": 1, "type": 1,
@ -656,7 +656,7 @@
}, },
{ {
"type": 0, "type": 0,
"value": " in the box below to confirm." "value": " στο παρακάτω πλαίσιο για επιβεβαίωση."
} }
], ],
"metrics.actions": [ "metrics.actions": [

View File

@ -640,15 +640,7 @@
"message.type-reset": [ "message.type-reset": [
{ {
"type": 0, "type": 0,
"value": "Type " "value": "جهت اطمینان {reset} را در کادر زیر بنویسید."
},
{
"type": 1,
"value": "reset"
},
{
"type": 0,
"value": " in the box below to confirm."
} }
], ],
"metrics.actions": [ "metrics.actions": [

View File

@ -32,13 +32,13 @@
"label.all-events": [ "label.all-events": [
{ {
"type": 0, "type": 0,
"value": "All events" "value": "Kaikki tapahtumat"
} }
], ],
"label.all-time": [ "label.all-time": [
{ {
"type": 0, "type": 0,
"value": "All time" "value": "Alusta lähtien"
} }
], ],
"label.all-websites": [ "label.all-websites": [
@ -86,7 +86,7 @@
"label.custom-range": [ "label.custom-range": [
{ {
"type": 0, "type": 0,
"value": "Mukautettu jakso" "value": "Mukautettu ajanjakso"
} }
], ],
"label.dashboard": [ "label.dashboard": [
@ -176,7 +176,7 @@
"label.language": [ "label.language": [
{ {
"type": 0, "type": 0,
"value": "Language" "value": "Kieli"
} }
], ],
"label.last-days": [ "label.last-days": [
@ -190,7 +190,7 @@
}, },
{ {
"type": 0, "type": 0,
"value": " päivät" "value": " päivää"
} }
], ],
"label.last-hours": [ "label.last-hours": [
@ -204,7 +204,7 @@
}, },
{ {
"type": 0, "type": 0,
"value": " tunnit" "value": " tuntia"
} }
], ],
"label.logged-in-as": [ "label.logged-in-as": [
@ -250,7 +250,7 @@
"label.owner": [ "label.owner": [
{ {
"type": 0, "type": 0,
"value": "Owner" "value": "Omistaja"
} }
], ],
"label.password": [ "label.password": [
@ -274,7 +274,7 @@
"label.realtime": [ "label.realtime": [
{ {
"type": 0, "type": 0,
"value": "Reaaliaikainen" "value": "Juuri nyt"
} }
], ],
"label.realtime-logs": [ "label.realtime-logs": [
@ -304,7 +304,7 @@
"label.reset-website": [ "label.reset-website": [
{ {
"type": 0, "type": 0,
"value": "Reset statistics" "value": "Nollaa tilastot"
} }
], ],
"label.save": [ "label.save": [
@ -334,7 +334,7 @@
"label.theme": [ "label.theme": [
{ {
"type": 0, "type": 0,
"value": "Theme" "value": "Teema"
} }
], ],
"label.this-month": [ "label.this-month": [
@ -404,7 +404,7 @@
}, },
{ {
"type": 0, "type": 0,
"value": " nykyinen " "value": " "
}, },
{ {
"offset": 0, "offset": 0,
@ -413,7 +413,7 @@
"value": [ "value": [
{ {
"type": 0, "type": 0,
"value": "yksi" "value": "vierailija"
} }
] ]
}, },
@ -421,7 +421,7 @@
"value": [ "value": [
{ {
"type": 0, "type": 0,
"value": "muut" "value": "vierailijaa"
} }
] ]
} }
@ -434,7 +434,7 @@
"message.confirm-delete": [ "message.confirm-delete": [
{ {
"type": 0, "type": 0,
"value": "Haluatko varmasti poistaa " "value": "Haluatko varmasti poistaa sivuston "
}, },
{ {
"type": 1, "type": 1,
@ -448,7 +448,7 @@
"message.confirm-reset": [ "message.confirm-reset": [
{ {
"type": 0, "type": 0,
"value": "Are your sure you want to reset " "value": "Haluatko varmasti poistaa sivuston "
}, },
{ {
"type": 1, "type": 1,
@ -456,7 +456,7 @@
}, },
{ {
"type": 0, "type": 0,
"value": "'s statistics?" "value": " tilastot?"
} }
], ],
"message.copied": [ "message.copied": [
@ -474,7 +474,7 @@
"message.failure": [ "message.failure": [
{ {
"type": 0, "type": 0,
"value": "Jotain meni väärin." "value": "Jotain meni pieleen."
} }
], ],
"message.get-share-url": [ "message.get-share-url": [
@ -512,7 +512,7 @@
}, },
{ {
"type": 0, "type": 0,
"value": " käyttäen selainta " "value": " selaimella "
}, },
{ {
"type": 1, "type": 1,
@ -520,7 +520,7 @@
}, },
{ {
"type": 0, "type": 0,
"value": " " "value": " laitteella "
}, },
{ {
"type": 1, "type": 1,
@ -528,7 +528,7 @@
}, },
{ {
"type": 0, "type": 0,
"value": "-laitteella: " "value": " "
}, },
{ {
"type": 1, "type": 1,
@ -580,7 +580,7 @@
"message.reset-warning": [ "message.reset-warning": [
{ {
"type": 0, "type": 0,
"value": "All statistics for this website will be deleted, but your tracking code will remain intact." "value": "Kaikki sivuston tilastot poistetaan, mutta seurantakoodi pysyy muuttumattomana."
} }
], ],
"message.save-success": [ "message.save-success": [
@ -592,7 +592,7 @@
"message.share-url": [ "message.share-url": [
{ {
"type": 0, "type": 0,
"value": "Tämä on julkisesti jaettu URL-osoitteelle " "value": "Tämä on julkisesti jaettu URL sivustolle "
}, },
{ {
"type": 1, "type": 1,
@ -606,13 +606,13 @@
"message.toggle-charts": [ "message.toggle-charts": [
{ {
"type": 0, "type": 0,
"value": "Toggle charts" "value": "Kytke kaaviot päälle/pois"
} }
], ],
"message.track-stats": [ "message.track-stats": [
{ {
"type": 0, "type": 0,
"value": "Jos haluat seurata kohteen " "value": "Jos haluat seurata sivuston "
}, },
{ {
"type": 1, "type": 1,
@ -628,7 +628,7 @@
}, },
{ {
"type": 0, "type": 0,
"value": " osioon." "value": "-osioon."
} }
], ],
"message.type-delete": [ "message.type-delete": [
@ -642,13 +642,13 @@
}, },
{ {
"type": 0, "type": 0,
"value": " alla olevaan ruutuun vahvistaaksesi." "value": " alla olevaan kenttään vahvistaaksesi."
} }
], ],
"message.type-reset": [ "message.type-reset": [
{ {
"type": 0, "type": 0,
"value": "Type " "value": "Kirjoita "
}, },
{ {
"type": 1, "type": 1,
@ -656,7 +656,7 @@
}, },
{ {
"type": 0, "type": 0,
"value": " in the box below to confirm." "value": " alla olevaan kenttään vahvistaaksesi."
} }
], ],
"metrics.actions": [ "metrics.actions": [
@ -704,7 +704,7 @@
"metrics.device.mobile": [ "metrics.device.mobile": [
{ {
"type": 0, "type": 0,
"value": "Mobiili" "value": "Puhelin"
} }
], ],
"metrics.device.tablet": [ "metrics.device.tablet": [
@ -746,7 +746,7 @@
"metrics.languages": [ "metrics.languages": [
{ {
"type": 0, "type": 0,
"value": "Languages" "value": "Kielet"
} }
], ],
"metrics.operating-systems": [ "metrics.operating-systems": [
@ -758,7 +758,7 @@
"metrics.page-views": [ "metrics.page-views": [
{ {
"type": 0, "type": 0,
"value": "Sivun näyttökertoja" "value": "Sivun näyttökerrat"
} }
], ],
"metrics.pages": [ "metrics.pages": [
@ -776,13 +776,13 @@
"metrics.unique-visitors": [ "metrics.unique-visitors": [
{ {
"type": 0, "type": 0,
"value": "Uniikit vierailijat" "value": "Yksittäiset kävijät"
} }
], ],
"metrics.views": [ "metrics.views": [
{ {
"type": 0, "type": 0,
"value": "Näyttökertoja" "value": "Näyttökerrat"
} }
], ],
"metrics.visitors": [ "metrics.visitors": [

View File

@ -648,7 +648,7 @@
"message.type-reset": [ "message.type-reset": [
{ {
"type": 0, "type": 0,
"value": "Type " "value": "Skriva "
}, },
{ {
"type": 1, "type": 1,
@ -656,7 +656,7 @@
}, },
{ {
"type": 0, "type": 0,
"value": " in the box below to confirm." "value": " í feltið fyri at vátta"
} }
], ],
"metrics.actions": [ "metrics.actions": [

View File

@ -628,7 +628,7 @@
"message.type-reset": [ "message.type-reset": [
{ {
"type": 0, "type": 0,
"value": "Type " "value": "הקלידו "
}, },
{ {
"type": 1, "type": 1,
@ -636,7 +636,7 @@
}, },
{ {
"type": 0, "type": 0,
"value": " in the box below to confirm." "value": " בתיבה על מנת לאשר"
} }
], ],
"metrics.actions": [ "metrics.actions": [

View File

@ -636,7 +636,7 @@
"message.type-reset": [ "message.type-reset": [
{ {
"type": 0, "type": 0,
"value": "Type " "value": "ुष्टि करने के लिए नीचे दिए गए बॉक्स में "
}, },
{ {
"type": 1, "type": 1,
@ -644,7 +644,7 @@
}, },
{ {
"type": 0, "type": 0,
"value": " in the box below to confirm." "value": " टाइप करें।"
} }
], ],
"metrics.actions": [ "metrics.actions": [

View File

@ -648,7 +648,7 @@
"message.type-reset": [ "message.type-reset": [
{ {
"type": 0, "type": 0,
"value": "Type " "value": "Megerősítéshez írd be az alábbi mezőbe azt, hogy "
}, },
{ {
"type": 1, "type": 1,
@ -656,7 +656,7 @@
}, },
{ {
"type": 0, "type": 0,
"value": " in the box below to confirm." "value": "."
} }
], ],
"metrics.actions": [ "metrics.actions": [

View File

@ -616,7 +616,7 @@
"message.type-reset": [ "message.type-reset": [
{ {
"type": 0, "type": 0,
"value": "Type " "value": "Ketikkan "
}, },
{ {
"type": 1, "type": 1,
@ -624,7 +624,7 @@
}, },
{ {
"type": 0, "type": 0,
"value": " in the box below to confirm." "value": " pada kotak di bawah untuk konfirmasi."
} }
], ],
"metrics.actions": [ "metrics.actions": [

View File

@ -620,7 +620,7 @@
"message.type-reset": [ "message.type-reset": [
{ {
"type": 0, "type": 0,
"value": "Type " "value": "確認のため、下のフォームに"
}, },
{ {
"type": 1, "type": 1,
@ -628,7 +628,7 @@
}, },
{ {
"type": 0, "type": 0,
"value": " in the box below to confirm." "value": "と入力してください。"
} }
], ],
"metrics.actions": [ "metrics.actions": [

View File

@ -620,7 +620,7 @@
"message.type-reset": [ "message.type-reset": [
{ {
"type": 0, "type": 0,
"value": "Type " "value": "확인을 위해 아래 박스에 "
}, },
{ {
"type": 1, "type": 1,
@ -628,7 +628,7 @@
}, },
{ {
"type": 0, "type": 0,
"value": " in the box below to confirm." "value": "값을 입력하십시오."
} }
], ],
"metrics.actions": [ "metrics.actions": [

View File

@ -648,7 +648,7 @@
"message.type-reset": [ "message.type-reset": [
{ {
"type": 0, "type": 0,
"value": "Type " "value": "Доорх хэсэгт "
}, },
{ {
"type": 1, "type": 1,
@ -656,7 +656,7 @@
}, },
{ {
"type": 0, "type": 0,
"value": " in the box below to confirm." "value": " гэж бичиж баталгаажуулна уу."
} }
], ],
"metrics.actions": [ "metrics.actions": [

View File

@ -640,7 +640,7 @@
"message.type-reset": [ "message.type-reset": [
{ {
"type": 0, "type": 0,
"value": "Type " "value": "Taip "
}, },
{ {
"type": 1, "type": 1,
@ -648,7 +648,7 @@
}, },
{ {
"type": 0, "type": 0,
"value": " in the box below to confirm." "value": " di dalam kotak di bawah untuk pengesahan."
} }
], ],
"metrics.actions": [ "metrics.actions": [

View File

@ -652,7 +652,7 @@
"message.type-reset": [ "message.type-reset": [
{ {
"type": 0, "type": 0,
"value": "Type " "value": "Skriv inn "
}, },
{ {
"type": 1, "type": 1,
@ -660,7 +660,7 @@
}, },
{ {
"type": 0, "type": 0,
"value": " in the box below to confirm." "value": " i boksen nedenfor for å bekrefte."
} }
], ],
"metrics.actions": [ "metrics.actions": [

View File

@ -656,7 +656,7 @@
}, },
{ {
"type": 0, "type": 0,
"value": " in the box below to confirm." "value": " in onderstaande veld om dit te bevestigen."
} }
], ],
"metrics.actions": [ "metrics.actions": [

View File

@ -648,7 +648,7 @@
"message.type-reset": [ "message.type-reset": [
{ {
"type": 0, "type": 0,
"value": "Type " "value": "Wpisz "
}, },
{ {
"type": 1, "type": 1,
@ -656,7 +656,7 @@
}, },
{ {
"type": 0, "type": 0,
"value": " in the box below to confirm." "value": " w polu poniżej, aby potwierdzić."
} }
], ],
"metrics.actions": [ "metrics.actions": [

View File

@ -652,7 +652,7 @@
"message.type-reset": [ "message.type-reset": [
{ {
"type": 0, "type": 0,
"value": "Type " "value": "Escreva "
}, },
{ {
"type": 1, "type": 1,
@ -660,7 +660,7 @@
}, },
{ {
"type": 0, "type": 0,
"value": " in the box below to confirm." "value": " abaixo para continuar."
} }
], ],
"metrics.actions": [ "metrics.actions": [

View File

@ -648,7 +648,7 @@
"message.type-reset": [ "message.type-reset": [
{ {
"type": 0, "type": 0,
"value": "Type " "value": "Napíšte "
}, },
{ {
"type": 1, "type": 1,
@ -656,7 +656,7 @@
}, },
{ {
"type": 0, "type": 0,
"value": " in the box below to confirm." "value": " pre potvrdenie."
} }
], ],
"metrics.actions": [ "metrics.actions": [

View File

@ -648,7 +648,7 @@
"message.type-reset": [ "message.type-reset": [
{ {
"type": 0, "type": 0,
"value": "Type " "value": "V spodnje polje vnesite "
}, },
{ {
"type": 1, "type": 1,
@ -656,7 +656,7 @@
}, },
{ {
"type": 0, "type": 0,
"value": " in the box below to confirm." "value": " za potrditev."
} }
], ],
"metrics.actions": [ "metrics.actions": [

View File

@ -32,13 +32,13 @@
"label.all-events": [ "label.all-events": [
{ {
"type": 0, "type": 0,
"value": "All events" "value": "Alla händelser"
} }
], ],
"label.all-time": [ "label.all-time": [
{ {
"type": 0, "type": 0,
"value": "All time" "value": "Sedan början"
} }
], ],
"label.all-websites": [ "label.all-websites": [
@ -176,7 +176,7 @@
"label.language": [ "label.language": [
{ {
"type": 0, "type": 0,
"value": "Language" "value": "Språk"
} }
], ],
"label.last-days": [ "label.last-days": [
@ -250,7 +250,7 @@
"label.owner": [ "label.owner": [
{ {
"type": 0, "type": 0,
"value": "Owner" "value": "Ägare"
} }
], ],
"label.password": [ "label.password": [
@ -304,7 +304,7 @@
"label.reset-website": [ "label.reset-website": [
{ {
"type": 0, "type": 0,
"value": "Reset statistics" "value": "Återställ statistik"
} }
], ],
"label.save": [ "label.save": [
@ -334,7 +334,7 @@
"label.theme": [ "label.theme": [
{ {
"type": 0, "type": 0,
"value": "Theme" "value": "Tema"
} }
], ],
"label.this-month": [ "label.this-month": [
@ -452,7 +452,7 @@
"message.confirm-reset": [ "message.confirm-reset": [
{ {
"type": 0, "type": 0,
"value": "Are your sure you want to reset " "value": "Är du säker på att du vill återställa statistiken för "
}, },
{ {
"type": 1, "type": 1,
@ -460,7 +460,7 @@
}, },
{ {
"type": 0, "type": 0,
"value": "'s statistics?" "value": "?"
} }
], ],
"message.copied": [ "message.copied": [
@ -502,7 +502,7 @@
"message.incorrect-username-password": [ "message.incorrect-username-password": [
{ {
"type": 0, "type": 0,
"value": "Felaktikt användarnamn/lösenord." "value": "Felaktigt användarnamn/lösenord."
} }
], ],
"message.log.visitor": [ "message.log.visitor": [
@ -584,7 +584,7 @@
"message.reset-warning": [ "message.reset-warning": [
{ {
"type": 0, "type": 0,
"value": "All statistics for this website will be deleted, but your tracking code will remain intact." "value": "All statistik för webbsajten tas bort men spårningskoden förblir oförändrad."
} }
], ],
"message.save-success": [ "message.save-success": [
@ -596,7 +596,7 @@
"message.share-url": [ "message.share-url": [
{ {
"type": 0, "type": 0,
"value": "Det här är den offentliga delnings-URL:en " "value": "Det här är den offentliga delnings-URL:en för "
}, },
{ {
"type": 1, "type": 1,
@ -610,7 +610,7 @@
"message.toggle-charts": [ "message.toggle-charts": [
{ {
"type": 0, "type": 0,
"value": "Toggle charts" "value": "Visa/göm grafer"
} }
], ],
"message.track-stats": [ "message.track-stats": [
@ -652,7 +652,7 @@
"message.type-reset": [ "message.type-reset": [
{ {
"type": 0, "type": 0,
"value": "Type " "value": "Skriv "
}, },
{ {
"type": 1, "type": 1,
@ -660,7 +660,7 @@
}, },
{ {
"type": 0, "type": 0,
"value": " in the box below to confirm." "value": " i rutan nedan för att bekräfta."
} }
], ],
"metrics.actions": [ "metrics.actions": [
@ -750,7 +750,7 @@
"metrics.languages": [ "metrics.languages": [
{ {
"type": 0, "type": 0,
"value": "Languages" "value": "Språk"
} }
], ],
"metrics.operating-systems": [ "metrics.operating-systems": [

View File

@ -640,7 +640,7 @@
"message.type-reset": [ "message.type-reset": [
{ {
"type": 0, "type": 0,
"value": "Type " "value": "உறுதிப்படுத்த கீழே உள்ள பெட்டியில் "
}, },
{ {
"type": 1, "type": 1,
@ -648,7 +648,7 @@
}, },
{ {
"type": 0, "type": 0,
"value": " in the box below to confirm." "value": " என தட்டச்சு செய்க."
} }
], ],
"metrics.actions": [ "metrics.actions": [

View File

@ -612,7 +612,7 @@
"message.type-reset": [ "message.type-reset": [
{ {
"type": 0, "type": 0,
"value": "Type " "value": "Onaylamak için kutuya "
}, },
{ {
"type": 1, "type": 1,
@ -620,7 +620,7 @@
}, },
{ {
"type": 0, "type": 0,
"value": " in the box below to confirm." "value": " yazın."
} }
], ],
"metrics.actions": [ "metrics.actions": [

View File

@ -632,7 +632,7 @@
"message.type-reset": [ "message.type-reset": [
{ {
"type": 0, "type": 0,
"value": "Type " "value": "Nhập "
}, },
{ {
"type": 1, "type": 1,
@ -640,7 +640,7 @@
}, },
{ {
"type": 0, "type": 0,
"value": " in the box below to confirm." "value": " bên dưới để xác nhận."
} }
], ],
"metrics.actions": [ "metrics.actions": [

View File

@ -636,7 +636,7 @@
"message.type-reset": [ "message.type-reset": [
{ {
"type": 0, "type": 0,
"value": "Type " "value": "在下方输入框输入 "
}, },
{ {
"type": 1, "type": 1,
@ -644,7 +644,7 @@
}, },
{ {
"type": 0, "type": 0,
"value": " in the box below to confirm." "value": " 以确认删除。"
} }
], ],
"metrics.actions": [ "metrics.actions": [

View File

@ -632,7 +632,7 @@
"message.type-reset": [ "message.type-reset": [
{ {
"type": 0, "type": 0,
"value": "Type " "value": "在下方空格輸入 "
}, },
{ {
"type": 1, "type": 1,
@ -640,7 +640,7 @@
}, },
{ {
"type": 0, "type": 0,
"value": " in the box below to confirm." "value": " 確認"
} }
], ],
"metrics.actions": [ "metrics.actions": [

View File

@ -23,7 +23,7 @@ files.forEach(file => {
keys.forEach(key => { keys.forEach(key => {
const orig = messages[key]; const orig = messages[key];
const check = lang[key]; const check = lang[key];
const ignored = ignore[id]?.includes(key); const ignored = ignore[id] === '*' || ignore[id]?.includes(key);
if (!ignored && (!check || check === orig)) { if (!ignored && (!check || check === orig)) {
console.log(chalk.redBright('*'), chalk.greenBright(`${key}:`), orig); console.log(chalk.redBright('*'), chalk.greenBright(`${key}:`), orig);
@ -32,7 +32,7 @@ files.forEach(file => {
}); });
if (count === 0) { if (count === 0) {
console.log('**👍 Complete!**'); console.log('**Complete!**');
} }
} }
}); });

View File

@ -1,4 +1,4 @@
const fs = require('fs'); const fs = require('fs-extra');
const path = require('path'); const path = require('path');
const https = require('https'); const https = require('https');
const chalk = require('chalk'); const chalk = require('chalk');
@ -16,11 +16,9 @@ const asyncForEach = async (array, callback) => {
} }
}; };
if (!fs.existsSync(dest)) {
fs.mkdirSync(dest);
}
const download = async files => { const download = async files => {
await fs.ensureDir(dest);
await asyncForEach(files, async file => { await asyncForEach(files, async file => {
const locale = file.replace('-', '_').replace('.json', ''); const locale = file.replace('-', '_').replace('.json', '');

View File

@ -1,4 +1,4 @@
const fs = require('fs'); const fs = require('fs-extra');
const path = require('path'); const path = require('path');
const https = require('https'); const https = require('https');
const chalk = require('chalk'); const chalk = require('chalk');
@ -16,11 +16,9 @@ const asyncForEach = async (array, callback) => {
} }
}; };
if (!fs.existsSync(dest)) {
fs.mkdirSync(dest);
}
const download = async files => { const download = async files => {
await fs.ensureDir(dest);
await asyncForEach(files, async file => { await asyncForEach(files, async file => {
const locale = file.replace('-', '_').replace('.json', ''); const locale = file.replace('-', '_').replace('.json', '');

View File

@ -1,4 +1,4 @@
const fs = require('fs'); const fs = require('fs-extra');
const path = require('path'); const path = require('path');
const del = require('del'); const del = require('del');
const prettier = require('prettier'); const prettier = require('prettier');
@ -14,11 +14,10 @@ if (removed.length) {
console.log(removed.map(n => `${n} ${chalk.redBright('✗')}`).join('\n')); console.log(removed.map(n => `${n} ${chalk.redBright('✗')}`).join('\n'));
} }
if (!fs.existsSync(dest)) { async function run() {
fs.mkdirSync(dest); await fs.ensureDir(dest);
}
files.forEach(file => { files.forEach(file => {
const lang = require(`../lang/${file}`); const lang = require(`../lang/${file}`);
const keys = Object.keys(lang).sort(); const keys = Object.keys(lang).sort();
@ -32,4 +31,7 @@ files.forEach(file => {
fs.writeFileSync(path.resolve(dest, file), json); fs.writeFileSync(path.resolve(dest, file), json);
console.log(path.resolve(src, file), chalk.greenBright('->'), path.resolve(dest, file)); console.log(path.resolve(src, file), chalk.greenBright('->'), path.resolve(dest, file));
}); });
}
run();

10
scripts/postbuild.js Normal file
View File

@ -0,0 +1,10 @@
require('dotenv').config();
const sendTelemetry = require('./telemetry');
async function run() {
if (!process.env.TELEMETRY_DISABLE) {
await sendTelemetry();
}
}
run();

54
scripts/telemetry.js Normal file
View File

@ -0,0 +1,54 @@
const fs = require('fs-extra');
const path = require('path');
const os = require('os');
const retry = require('async-retry');
const isCI = require('is-ci');
const pkg = require('../package.json');
const dest = path.resolve(__dirname, '../.next/cache/umami.json');
const url = 'https://telemetry.umami.is/api/collect';
async function sendTelemetry() {
await fs.ensureFile(dest);
let json = {};
try {
json = await fs.readJSON(dest);
} catch {
// Ignore
}
if (json.version !== pkg.version) {
const { default: isDocker } = await import('is-docker');
const { default: fetch } = await import('node-fetch');
await fs.writeJSON(dest, { version: pkg.version });
const payload = {
umami: pkg.version,
node: process.version,
platform: os.platform(),
arch: os.arch(),
os: `${os.type()} (${os.version()})`,
isDocker: isDocker(),
isCI,
};
await retry(
async () => {
await fetch(url, {
method: 'post',
cache: 'no-cache',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(payload),
});
},
{ minTimeout: 500, retries: 1, factor: 1 },
).catch(() => {});
}
}
module.exports = sendTelemetry;

View File

@ -9,7 +9,7 @@ import {
} from 'lib/constants'; } from 'lib/constants';
import { getItem } from 'lib/web'; import { getItem } from 'lib/web';
const defaultDashboardConfig = { export const defaultDashboardConfig = {
showCharts: true, showCharts: true,
limit: DEFAULT_WEBSITE_LIMIT, limit: DEFAULT_WEBSITE_LIMIT,
}; };

View File

@ -22,6 +22,7 @@ import { removeTrailingSlash } from '../lib/url';
const autoTrack = attr('data-auto-track') !== 'false'; const autoTrack = attr('data-auto-track') !== 'false';
const dnt = attr('data-do-not-track'); const dnt = attr('data-do-not-track');
const useCache = attr('data-cache'); const useCache = attr('data-cache');
const cssEvents = attr('data-css-events') !== 'false';
const domain = attr('data-domains') || ''; const domain = attr('data-domains') || '';
const domains = domain.split(',').map(n => n.trim()); const domains = domain.split(',').map(n => n.trim());
@ -29,7 +30,7 @@ import { removeTrailingSlash } from '../lib/url';
const eventSelect = "[class*='umami--']"; const eventSelect = "[class*='umami--']";
const cacheKey = 'umami.cache'; const cacheKey = 'umami.cache';
const disableTracking = () => const trackingDisabled = () =>
(localStorage && localStorage.getItem('umami.disabled')) || (localStorage && localStorage.getItem('umami.disabled')) ||
(dnt && doNotTrack()) || (dnt && doNotTrack()) ||
(domain && !domains.includes(hostname)); (domain && !domains.includes(hostname));
@ -47,7 +48,7 @@ import { removeTrailingSlash } from '../lib/url';
const post = (url, data, callback) => { const post = (url, data, callback) => {
const req = new XMLHttpRequest(); const req = new XMLHttpRequest();
req.open('POST', url, true); req.open('POST', url, true);
req.setRequestHeader('Content-Type', 'application/json'); req.setRequestHeader('Content-Type', 'text/plain');
req.onreadystatechange = () => { req.onreadystatechange = () => {
if (req.readyState === 4) { if (req.readyState === 4) {
@ -58,20 +59,24 @@ import { removeTrailingSlash } from '../lib/url';
req.send(JSON.stringify(data)); req.send(JSON.stringify(data));
}; };
const collect = (type, params, uuid) => { const getPayload = () => ({
if (disableTracking()) return; website,
const payload = {
website: uuid,
hostname, hostname,
screen, screen,
language, language,
cache: useCache && sessionStorage.getItem(cacheKey), cache: useCache && sessionStorage.getItem(cacheKey),
url: currentUrl,
});
const assign = (a, b) => {
Object.keys(b).forEach(key => {
a[key] = b[key];
});
return a;
}; };
Object.keys(params).forEach(key => { const collect = (type, payload) => {
payload[key] = params[key]; if (trackingDisabled()) return;
});
post( post(
`${root}/api/collect`, `${root}/api/collect`,
@ -86,28 +91,42 @@ import { removeTrailingSlash } from '../lib/url';
const trackView = (url = currentUrl, referrer = currentRef, uuid = website) => { const trackView = (url = currentUrl, referrer = currentRef, uuid = website) => {
collect( collect(
'pageview', 'pageview',
{ assign(getPayload(), {
website: uuid,
url, url,
referrer, referrer,
}, }),
uuid,
); );
}; };
const trackEvent = (event_value, event_type = 'custom', url = currentUrl, uuid = website) => { const trackEvent = (event_value, event_type = 'custom', url = currentUrl, uuid = website) => {
collect( collect(
'event', 'event',
{ assign(getPayload(), {
website: uuid,
url,
event_type, event_type,
event_value, event_value,
url, }),
},
uuid,
); );
}; };
/* Handle events */ /* Handle events */
const sendEvent = (value, type) => {
const payload = getPayload();
payload.event_type = type;
payload.event_value = value;
const data = JSON.stringify({
type: 'event',
payload,
});
navigator.sendBeacon(`${root}/api/collect`, data);
};
const addEvents = node => { const addEvents = node => {
const elements = node.querySelectorAll(eventSelect); const elements = node.querySelectorAll(eventSelect);
Array.prototype.forEach.call(elements, addEvent); Array.prototype.forEach.call(elements, addEvent);
@ -120,20 +139,18 @@ import { removeTrailingSlash } from '../lib/url';
const [, type, value] = className.split('--'); const [, type, value] = className.split('--');
const listener = listeners[className] const listener = listeners[className]
? listeners[className] ? listeners[className]
: (listeners[className] = () => trackEvent(value, type)); : (listeners[className] = () => {
if (element.tagName === 'A') {
sendEvent(value, type);
} else {
trackEvent(value, type);
}
});
element.addEventListener(type, listener, true); element.addEventListener(type, listener, true);
}); });
}; };
const monitorMutate = mutations => {
mutations.forEach(mutation => {
const element = mutation.target;
addEvent(element);
addEvents(element);
});
};
/* Handle history changes */ /* Handle history changes */
const handlePush = (state, title, url) => { const handlePush = (state, title, url) => {
@ -153,6 +170,19 @@ import { removeTrailingSlash } from '../lib/url';
} }
}; };
const observeDocument = () => {
const monitorMutate = mutations => {
mutations.forEach(mutation => {
const element = mutation.target;
addEvent(element);
addEvents(element);
});
};
const observer = new MutationObserver(monitorMutate);
observer.observe(document, { childList: true, subtree: true });
};
/* Global */ /* Global */
if (!window.umami) { if (!window.umami) {
@ -165,20 +195,23 @@ import { removeTrailingSlash } from '../lib/url';
/* Start */ /* Start */
if (autoTrack && !disableTracking()) { if (autoTrack && !trackingDisabled()) {
history.pushState = hook(history, 'pushState', handlePush); history.pushState = hook(history, 'pushState', handlePush);
history.replaceState = hook(history, 'replaceState', handlePush); history.replaceState = hook(history, 'replaceState', handlePush);
const update = () => { const update = () => {
if (document.readyState === 'complete') { if (document.readyState === 'complete') {
addEvents(document);
trackView(); trackView();
const observer = new MutationObserver(monitorMutate); if (cssEvents) {
observer.observe(document, { childList: true, subtree: true }); addEvents(document);
observeDocument();
}
} }
}; };
document.addEventListener('readystatechange', update, true); document.addEventListener('readystatechange', update, true);
update(); update();
} }
})(window); })(window);

1638
yarn.lock

File diff suppressed because it is too large Load Diff