mirror of
https://github.com/umami-software/umami.git
synced 2026-02-19 12:05:41 +01:00
Merge branch 'mikecao:master' into master
This commit is contained in:
commit
d7144ee485
73 changed files with 2206 additions and 1086 deletions
36
.github/workflows/cd-manual.yml
vendored
Normal file
36
.github/workflows/cd-manual.yml
vendored
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
name: Create docker images
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
version:
|
||||||
|
type: string
|
||||||
|
description: Version
|
||||||
|
required: true
|
||||||
|
add-latest:
|
||||||
|
type: boolean
|
||||||
|
description: Add latest tag
|
||||||
|
required: false
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
name: Build, push, and deploy
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
db-type: [postgresql, mysql]
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
|
||||||
|
- uses: mr-smithers-excellent/docker-build-push@v5
|
||||||
|
name: Build & push Docker image for ${{ matrix.db-type }}
|
||||||
|
with:
|
||||||
|
image: umami
|
||||||
|
tags: ${{ matrix.db-type }}-${{ inputs.version }}
|
||||||
|
addLatest: ${{ inputs.add-latest }}
|
||||||
|
buildArgs: DATABASE_TYPE=${{ matrix.db-type }}
|
||||||
|
registry: ghcr.io/${{ github.actor }}
|
||||||
|
username: ${{ github.actor }}
|
||||||
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
24
.github/workflows/cd.yml
vendored
24
.github/workflows/cd.yml
vendored
|
|
@ -14,17 +14,17 @@ jobs:
|
||||||
db-type: [postgresql, mysql]
|
db-type: [postgresql, mysql]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
|
|
||||||
- name: Set env
|
- name: Set env
|
||||||
run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV
|
run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV
|
||||||
|
|
||||||
- uses: mr-smithers-excellent/docker-build-push@v5
|
- uses: mr-smithers-excellent/docker-build-push@v5
|
||||||
name: Build & push Docker image for ${{ matrix.db-type }}
|
name: Build & push Docker image for ${{ matrix.db-type }}
|
||||||
with:
|
with:
|
||||||
image: umami
|
image: umami
|
||||||
tags: ${{ matrix.db-type }}-${{ env.RELEASE_VERSION }}, ${{ matrix.db-type }}-latest
|
tags: ${{ matrix.db-type }}-${{ env.RELEASE_VERSION }}, ${{ matrix.db-type }}-latest
|
||||||
buildArgs: DATABASE_TYPE=${{ matrix.db-type }}
|
buildArgs: DATABASE_TYPE=${{ matrix.db-type }}
|
||||||
registry: ghcr.io/${{ github.actor }}
|
registry: ghcr.io/${{ github.actor }}
|
||||||
username: ${{ github.actor }}
|
username: ${{ github.actor }}
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
|
||||||
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -11,7 +11,7 @@
|
||||||
# next.js
|
# next.js
|
||||||
/.next/
|
/.next/
|
||||||
/out/
|
/out/
|
||||||
/prisma/schema.prisma
|
/prisma/
|
||||||
|
|
||||||
# production
|
# production
|
||||||
/build
|
/build
|
||||||
|
|
|
||||||
82
Dockerfile
82
Dockerfile
|
|
@ -1,45 +1,59 @@
|
||||||
# Build image
|
# Install dependencies only when needed
|
||||||
FROM node:12.22-alpine AS build
|
FROM node:16-alpine AS deps
|
||||||
ARG BASE_PATH
|
# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed.
|
||||||
ARG DATABASE_TYPE
|
RUN apk add --no-cache libc6-compat
|
||||||
|
WORKDIR /app
|
||||||
ENV BASE_PATH=$BASE_PATH
|
COPY package.json yarn.lock ./
|
||||||
ENV DATABASE_URL "postgresql://umami:umami@db:5432/umami"
|
|
||||||
ENV DATABASE_TYPE=$DATABASE_TYPE
|
|
||||||
|
|
||||||
WORKDIR /build
|
|
||||||
|
|
||||||
RUN yarn config set --home enableTelemetry 0
|
|
||||||
COPY package.json yarn.lock /build/
|
|
||||||
|
|
||||||
# Install only the production dependencies
|
|
||||||
RUN yarn install --production --frozen-lockfile
|
|
||||||
|
|
||||||
# Cache these modules for production
|
|
||||||
RUN cp -R node_modules/ prod_node_modules/
|
|
||||||
|
|
||||||
# Install development dependencies
|
|
||||||
RUN yarn install --frozen-lockfile
|
RUN yarn install --frozen-lockfile
|
||||||
|
|
||||||
COPY . /build
|
# Rebuild the source code only when needed
|
||||||
RUN yarn next telemetry disable
|
FROM node:16-alpine AS builder
|
||||||
|
WORKDIR /app
|
||||||
|
COPY --from=deps /app/node_modules ./node_modules
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
ARG DATABASE_URL
|
||||||
|
ARG DATABASE_TYPE
|
||||||
|
ARG BASE_PATH
|
||||||
|
ARG DISABLE_LOGIN
|
||||||
|
|
||||||
|
ENV DATABASE_URL $DATABASE_URL
|
||||||
|
ENV DATABASE_TYPE $DATABASE_TYPE
|
||||||
|
ENV BASE_PATH $BASE_PATH
|
||||||
|
ENV DISABLE_LOGIN $DISABLE_LOGIN
|
||||||
|
|
||||||
|
ENV NEXT_TELEMETRY_DISABLED 1
|
||||||
|
|
||||||
RUN yarn build
|
RUN yarn build
|
||||||
|
|
||||||
# Production image
|
# Production image, copy all the files and run next
|
||||||
FROM node:12.22-alpine AS production
|
FROM node:16-alpine AS runner
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# Copy cached dependencies
|
ENV NODE_ENV production
|
||||||
COPY --from=build /build/prod_node_modules ./node_modules
|
ENV NEXT_TELEMETRY_DISABLED 1
|
||||||
|
|
||||||
# Copy generated Prisma client
|
RUN addgroup --system --gid 1001 nodejs
|
||||||
COPY --from=build /build/node_modules/.prisma/ ./node_modules/.prisma/
|
RUN adduser --system --uid 1001 nextjs
|
||||||
|
|
||||||
COPY --from=build /build/yarn.lock /build/package.json ./
|
RUN yarn global add prisma
|
||||||
COPY --from=build /build/.next ./.next
|
|
||||||
COPY --from=build /build/public ./public
|
|
||||||
|
|
||||||
USER node
|
# You only need to copy next.config.js if you are NOT using the default configuration
|
||||||
|
COPY --from=builder /app/next.config.js ./
|
||||||
|
COPY --from=builder /app/public ./public
|
||||||
|
COPY --from=builder /app/package.json ./package.json
|
||||||
|
COPY --from=builder /app/prisma/schema.prisma ./prisma/schema.prisma
|
||||||
|
COPY --from=builder /app/prisma/migrations ./prisma/migrations
|
||||||
|
|
||||||
|
# Automatically leverage output traces to reduce image size
|
||||||
|
# https://nextjs.org/docs/advanced-features/output-file-tracing
|
||||||
|
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
|
||||||
|
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
|
||||||
|
|
||||||
|
USER nextjs
|
||||||
|
|
||||||
EXPOSE 3000
|
EXPOSE 3000
|
||||||
CMD ["yarn", "start"]
|
|
||||||
|
ENV PORT 3000
|
||||||
|
|
||||||
|
CMD ["yarn", "start-docker"]
|
||||||
|
|
|
||||||
53
README.md
53
README.md
|
|
@ -10,43 +10,29 @@ A detailed getting started guide can be found at [https://umami.is/docs/](https:
|
||||||
|
|
||||||
### Requirements
|
### Requirements
|
||||||
|
|
||||||
- A server with Node.js 12 or newer
|
- A server with Node.js version 12 or newer
|
||||||
- A database (MySQL or Postgresql)
|
- A database. Umami supports [MySQL](https://www.mysql.com/) and [Postgresql](https://www.postgresql.org/) databases.
|
||||||
|
|
||||||
|
### Install Yarn
|
||||||
|
|
||||||
|
```
|
||||||
|
npm install -g yarn
|
||||||
|
```
|
||||||
|
|
||||||
### Get the source code and install packages
|
### Get the source code and install packages
|
||||||
|
|
||||||
```
|
```
|
||||||
git clone https://github.com/mikecao/umami.git
|
git clone https://github.com/mikecao/umami.git
|
||||||
cd umami
|
cd umami
|
||||||
npm install
|
yarn install
|
||||||
```
|
```
|
||||||
|
|
||||||
### Create database tables
|
|
||||||
|
|
||||||
Umami supports [MySQL](https://www.mysql.com/) and [Postgresql](https://www.postgresql.org/).
|
|
||||||
Create a database for your Umami installation and install the tables with the included scripts.
|
|
||||||
|
|
||||||
For MySQL:
|
|
||||||
|
|
||||||
```
|
|
||||||
mysql -u username -p databasename < sql/schema.mysql.sql
|
|
||||||
```
|
|
||||||
|
|
||||||
For Postgresql:
|
|
||||||
|
|
||||||
```
|
|
||||||
psql -h hostname -U username -d databasename -f sql/schema.postgresql.sql
|
|
||||||
```
|
|
||||||
|
|
||||||
This will also create a login account with username **admin** and password **umami**.
|
|
||||||
|
|
||||||
### Configure umami
|
### Configure umami
|
||||||
|
|
||||||
Create an `.env` file with the following
|
Create an `.env` file with the following
|
||||||
|
|
||||||
```
|
```
|
||||||
DATABASE_URL=(connection url)
|
DATABASE_URL=(connection url)
|
||||||
HASH_SALT=(any random string)
|
|
||||||
```
|
```
|
||||||
|
|
||||||
The connection url is in the following format:
|
The connection url is in the following format:
|
||||||
|
|
@ -56,21 +42,27 @@ postgresql://username:mypassword@localhost:5432/mydb
|
||||||
mysql://username:mypassword@localhost:3306/mydb
|
mysql://username:mypassword@localhost:3306/mydb
|
||||||
```
|
```
|
||||||
|
|
||||||
The `HASH_SALT` is used to generate unique values for your installation.
|
|
||||||
|
|
||||||
### Build the application
|
### Build the application
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm run build
|
yarn build
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Create database tables
|
||||||
|
|
||||||
|
```bash
|
||||||
|
yarn update-db
|
||||||
|
```
|
||||||
|
|
||||||
|
This will also create a login account with username **admin** and password **umami**.
|
||||||
|
|
||||||
### Start the application
|
### Start the application
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm start
|
yarn start
|
||||||
```
|
```
|
||||||
|
|
||||||
By default this will launch the application on `http://localhost:3000`. You will need to either
|
By default this will launch the application on `http://localhost:3000`. You will need to either
|
||||||
[proxy](https://docs.nginx.com/nginx/admin-guide/web-server/reverse-proxy/) requests from your web server
|
[proxy](https://docs.nginx.com/nginx/admin-guide/web-server/reverse-proxy/) requests from your web server
|
||||||
or change the [port](https://nextjs.org/docs/api-reference/cli#production) to serve the application directly.
|
or change the [port](https://nextjs.org/docs/api-reference/cli#production) to serve the application directly.
|
||||||
|
|
||||||
|
|
@ -98,8 +90,9 @@ To get the latest features, simply do a pull, install any new dependencies, and
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git pull
|
git pull
|
||||||
npm install
|
yarn install
|
||||||
npm run build
|
yarn build
|
||||||
|
yarn update-db
|
||||||
```
|
```
|
||||||
|
|
||||||
To update the Docker image, simply pull the new images and rebuild:
|
To update the Docker image, simply pull the new images and rebuild:
|
||||||
|
|
|
||||||
38
app.json
38
app.json
|
|
@ -1,26 +1,16 @@
|
||||||
{
|
{
|
||||||
"name": "Umami",
|
"name": "Umami",
|
||||||
"description": "Umami is a simple, fast, website analytics alternative to Google Analytics.",
|
"description": "Umami is a simple, fast, website analytics alternative to Google Analytics.",
|
||||||
"keywords": [
|
"keywords": ["analytics", "charts", "statistics", "web-analytics"],
|
||||||
"analytics",
|
"website": "https://umami.is",
|
||||||
"charts",
|
"repository": "https://github.com/mikecao/umami",
|
||||||
"statistics",
|
"addons": ["heroku-postgresql"],
|
||||||
"web-analytics"
|
"env": {
|
||||||
],
|
"HASH_SALT": {
|
||||||
"website": "https://umami.is",
|
"description": "Used to generate unique values for your installation",
|
||||||
"repository": "https://github.com/mikecao/umami",
|
"required": true,
|
||||||
"addons": [
|
"generator": "secret"
|
||||||
"heroku-postgresql"
|
}
|
||||||
],
|
},
|
||||||
"env": {
|
"success_url": "/"
|
||||||
"HASH_SALT": {
|
|
||||||
"description": "Used to generate unique values for your installation",
|
|
||||||
"required": true,
|
|
||||||
"generator": "secret"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"scripts": {
|
|
||||||
"postdeploy": "psql $DATABASE_URL -f sql/schema.postgresql.sql"
|
|
||||||
},
|
|
||||||
"success_url": "/"
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,27 +1,38 @@
|
||||||
import React from 'react';
|
import { useState, useEffect, useCallback } from 'react';
|
||||||
import { FormattedMessage } from 'react-intl';
|
import { FormattedMessage } from 'react-intl';
|
||||||
import useVersion from 'hooks/useVersion';
|
import ButtonLayout from 'components/layout/ButtonLayout';
|
||||||
import styles from './UpdateNotice.module.css';
|
import useStore, { checkVersion } from 'store/version';
|
||||||
import ButtonLayout from '../layout/ButtonLayout';
|
import { setItem } from 'lib/web';
|
||||||
|
import { VERSION_CHECK, VERSION_URL } from 'lib/constants';
|
||||||
import Button from './Button';
|
import Button from './Button';
|
||||||
import useForceUpdate from '../../hooks/useForceUpdate';
|
import styles from './UpdateNotice.module.css';
|
||||||
|
|
||||||
export default function UpdateNotice() {
|
export default function UpdateNotice() {
|
||||||
const forceUpdate = useForceUpdate();
|
const { latest, checked, hasUpdate } = useStore();
|
||||||
const { hasUpdate, checked, latest, updateCheck } = useVersion(true);
|
const [dismissed, setDismissed] = useState(false);
|
||||||
|
|
||||||
|
const updateCheck = useCallback(() => {
|
||||||
|
setItem(VERSION_CHECK, { version: latest, time: Date.now() });
|
||||||
|
}, [latest]);
|
||||||
|
|
||||||
function handleViewClick() {
|
function handleViewClick() {
|
||||||
location.href = 'https://github.com/mikecao/umami/releases';
|
|
||||||
updateCheck();
|
updateCheck();
|
||||||
forceUpdate();
|
setDismissed(true);
|
||||||
|
location.href = VERSION_URL;
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleDismissClick() {
|
function handleDismissClick() {
|
||||||
updateCheck();
|
updateCheck();
|
||||||
forceUpdate();
|
setDismissed(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!hasUpdate || checked) {
|
useEffect(() => {
|
||||||
|
if (!checked) {
|
||||||
|
checkVersion();
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (!hasUpdate || dismissed) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,11 +3,11 @@ import classNames from 'classnames';
|
||||||
import { FormattedMessage } from 'react-intl';
|
import { FormattedMessage } from 'react-intl';
|
||||||
import Link from 'components/common/Link';
|
import Link from 'components/common/Link';
|
||||||
import styles from './Footer.module.css';
|
import styles from './Footer.module.css';
|
||||||
import useVersion from 'hooks/useVersion';
|
import useStore from 'store/version';
|
||||||
import { HOMEPAGE_URL, VERSION_URL } from 'lib/constants';
|
import { HOMEPAGE_URL, VERSION_URL } from 'lib/constants';
|
||||||
|
|
||||||
export default function Footer() {
|
export default function Footer() {
|
||||||
const { current } = useVersion();
|
const { current } = useStore();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<footer className={classNames(styles.footer, 'row')}>
|
<footer className={classNames(styles.footer, 'row')}>
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ export default function Header() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{user?.is_admin && <UpdateNotice />}
|
{user?.is_admin && !process.env.updatesDisabled && <UpdateNotice />}
|
||||||
<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} />
|
||||||
|
|
|
||||||
|
|
@ -3,20 +3,16 @@ import { FormattedMessage } from 'react-intl';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import useFetch from 'hooks/useFetch';
|
import useFetch from 'hooks/useFetch';
|
||||||
import Dot from 'components/common/Dot';
|
import Dot from 'components/common/Dot';
|
||||||
import { TOKEN_HEADER } from 'lib/constants';
|
|
||||||
import useShareToken from 'hooks/useShareToken';
|
|
||||||
import styles from './ActiveUsers.module.css';
|
import styles from './ActiveUsers.module.css';
|
||||||
|
|
||||||
export default function ActiveUsers({ websiteId, className, value, interval = 60000 }) {
|
export default function ActiveUsers({ websiteId, className, value, interval = 60000 }) {
|
||||||
const shareToken = useShareToken();
|
|
||||||
const url = websiteId ? `/website/${websiteId}/active` : null;
|
const url = websiteId ? `/website/${websiteId}/active` : null;
|
||||||
const { data } = useFetch(url, {
|
const { data } = useFetch(url, {
|
||||||
interval,
|
interval,
|
||||||
headers: { [TOKEN_HEADER]: shareToken?.token },
|
|
||||||
});
|
});
|
||||||
const count = useMemo(() => {
|
const count = useMemo(() => {
|
||||||
if (websiteId) {
|
if (websiteId) {
|
||||||
return data?.[0]?.x || 0
|
return data?.[0]?.x || 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
return value !== undefined ? value : 0;
|
return value !== undefined ? value : 0;
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,21 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import MetricsTable from './MetricsTable';
|
import MetricsTable from './MetricsTable';
|
||||||
import { percentFilter } from 'lib/filters';
|
import { percentFilter } from 'lib/filters';
|
||||||
import { FormattedMessage } from 'react-intl';
|
import { useIntl, defineMessages } from 'react-intl';
|
||||||
import FilterLink from 'components/common/FilterLink';
|
import FilterLink from 'components/common/FilterLink';
|
||||||
import useCountryNames from 'hooks/useCountryNames';
|
import useCountryNames from 'hooks/useCountryNames';
|
||||||
import useLocale from 'hooks/useLocale';
|
import useLocale from 'hooks/useLocale';
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
unknown: { id: 'label.unknown', defaultMessage: 'Unknown' },
|
||||||
|
countries: { id: 'metrics.countries', defaultMessage: 'Countries' },
|
||||||
|
visitors: { id: 'metrics.visitors', defaultMessage: 'Visitors' },
|
||||||
|
});
|
||||||
|
|
||||||
export default function CountriesTable({ websiteId, onDataLoad, ...props }) {
|
export default function CountriesTable({ websiteId, onDataLoad, ...props }) {
|
||||||
const { locale } = useLocale();
|
const { locale } = useLocale();
|
||||||
const countryNames = useCountryNames(locale);
|
const countryNames = useCountryNames(locale);
|
||||||
|
const { formatMessage } = useIntl();
|
||||||
|
|
||||||
function renderLink({ x: code }) {
|
function renderLink({ x: code }) {
|
||||||
return (
|
return (
|
||||||
|
|
@ -16,9 +23,7 @@ export default function CountriesTable({ websiteId, onDataLoad, ...props }) {
|
||||||
<FilterLink
|
<FilterLink
|
||||||
id="country"
|
id="country"
|
||||||
value={code}
|
value={code}
|
||||||
label={
|
label={countryNames[code] ?? formatMessage(messages.unknown)}
|
||||||
countryNames[code] ?? <FormattedMessage id="label.unknown" defaultMessage="Unknown" />
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
@ -27,9 +32,9 @@ export default function CountriesTable({ websiteId, onDataLoad, ...props }) {
|
||||||
return (
|
return (
|
||||||
<MetricsTable
|
<MetricsTable
|
||||||
{...props}
|
{...props}
|
||||||
title={<FormattedMessage id="metrics.countries" defaultMessage="Countries" />}
|
title={formatMessage(messages.countries)}
|
||||||
type="country"
|
type="country"
|
||||||
metric={<FormattedMessage id="metrics.visitors" defaultMessage="Visitors" />}
|
metric={formatMessage(messages.visitors)}
|
||||||
websiteId={websiteId}
|
websiteId={websiteId}
|
||||||
onDataLoad={data => onDataLoad?.(percentFilter(data))}
|
onDataLoad={data => onDataLoad?.(percentFilter(data))}
|
||||||
renderLabel={renderLink}
|
renderLabel={renderLink}
|
||||||
|
|
|
||||||
|
|
@ -6,8 +6,7 @@ import useFetch from 'hooks/useFetch';
|
||||||
import useDateRange from 'hooks/useDateRange';
|
import useDateRange from 'hooks/useDateRange';
|
||||||
import useTimezone from 'hooks/useTimezone';
|
import useTimezone from 'hooks/useTimezone';
|
||||||
import usePageQuery from 'hooks/usePageQuery';
|
import usePageQuery from 'hooks/usePageQuery';
|
||||||
import useShareToken from 'hooks/useShareToken';
|
import { EVENT_COLORS } 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 [{ startDate, endDate, unit, modified }] = useDateRange(websiteId);
|
const [{ startDate, endDate, unit, modified }] = useDateRange(websiteId);
|
||||||
|
|
@ -15,7 +14,6 @@ export default function EventsChart({ websiteId, className, token }) {
|
||||||
const {
|
const {
|
||||||
query: { url, eventType },
|
query: { url, eventType },
|
||||||
} = usePageQuery();
|
} = usePageQuery();
|
||||||
const shareToken = useShareToken();
|
|
||||||
|
|
||||||
const { data, loading } = useFetch(
|
const { data, loading } = useFetch(
|
||||||
`/website/${websiteId}/events`,
|
`/website/${websiteId}/events`,
|
||||||
|
|
@ -29,7 +27,6 @@ export default function EventsChart({ websiteId, className, token }) {
|
||||||
event_type: eventType,
|
event_type: eventType,
|
||||||
token,
|
token,
|
||||||
},
|
},
|
||||||
headers: { [TOKEN_HEADER]: shareToken?.token },
|
|
||||||
},
|
},
|
||||||
[modified, eventType],
|
[modified, eventType],
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -6,14 +6,11 @@ import ErrorMessage from 'components/common/ErrorMessage';
|
||||||
import useFetch from 'hooks/useFetch';
|
import useFetch from 'hooks/useFetch';
|
||||||
import useDateRange from 'hooks/useDateRange';
|
import useDateRange from 'hooks/useDateRange';
|
||||||
import usePageQuery from 'hooks/usePageQuery';
|
import usePageQuery from 'hooks/usePageQuery';
|
||||||
import useShareToken from 'hooks/useShareToken';
|
|
||||||
import { formatShortTime, formatNumber, formatLongNumber } from 'lib/format';
|
import { formatShortTime, formatNumber, formatLongNumber } from 'lib/format';
|
||||||
import { TOKEN_HEADER } from 'lib/constants';
|
|
||||||
import MetricCard from './MetricCard';
|
import MetricCard from './MetricCard';
|
||||||
import styles from './MetricsBar.module.css';
|
import styles from './MetricsBar.module.css';
|
||||||
|
|
||||||
export default function MetricsBar({ websiteId, className }) {
|
export default function MetricsBar({ websiteId, className }) {
|
||||||
const shareToken = useShareToken();
|
|
||||||
const [dateRange] = useDateRange(websiteId);
|
const [dateRange] = useDateRange(websiteId);
|
||||||
const { startDate, endDate, modified } = dateRange;
|
const { startDate, endDate, modified } = dateRange;
|
||||||
const [format, setFormat] = useState(true);
|
const [format, setFormat] = useState(true);
|
||||||
|
|
@ -34,7 +31,6 @@ export default function MetricsBar({ websiteId, className }) {
|
||||||
device,
|
device,
|
||||||
country,
|
country,
|
||||||
},
|
},
|
||||||
headers: { [TOKEN_HEADER]: shareToken?.token },
|
|
||||||
},
|
},
|
||||||
[modified, url, referrer, os, browser, device, country],
|
[modified, url, referrer, os, browser, device, country],
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -9,10 +9,9 @@ import Arrow from 'assets/arrow-right.svg';
|
||||||
import { percentFilter } from 'lib/filters';
|
import { percentFilter } from 'lib/filters';
|
||||||
import useDateRange from 'hooks/useDateRange';
|
import useDateRange from 'hooks/useDateRange';
|
||||||
import usePageQuery from 'hooks/usePageQuery';
|
import usePageQuery from 'hooks/usePageQuery';
|
||||||
import useShareToken from 'hooks/useShareToken';
|
|
||||||
import ErrorMessage from 'components/common/ErrorMessage';
|
import ErrorMessage from 'components/common/ErrorMessage';
|
||||||
import DataTable from './DataTable';
|
import DataTable from './DataTable';
|
||||||
import { DEFAULT_ANIMATION_DURATION, TOKEN_HEADER } from 'lib/constants';
|
import { DEFAULT_ANIMATION_DURATION } from 'lib/constants';
|
||||||
import styles from './MetricsTable.module.css';
|
import styles from './MetricsTable.module.css';
|
||||||
|
|
||||||
export default function MetricsTable({
|
export default function MetricsTable({
|
||||||
|
|
@ -25,7 +24,6 @@ export default function MetricsTable({
|
||||||
onDataLoad,
|
onDataLoad,
|
||||||
...props
|
...props
|
||||||
}) {
|
}) {
|
||||||
const shareToken = useShareToken();
|
|
||||||
const [{ startDate, endDate, modified }] = useDateRange(websiteId);
|
const [{ startDate, endDate, modified }] = useDateRange(websiteId);
|
||||||
const {
|
const {
|
||||||
resolve,
|
resolve,
|
||||||
|
|
@ -49,7 +47,6 @@ export default function MetricsTable({
|
||||||
},
|
},
|
||||||
onDataLoad,
|
onDataLoad,
|
||||||
delay: DEFAULT_ANIMATION_DURATION,
|
delay: DEFAULT_ANIMATION_DURATION,
|
||||||
headers: { [TOKEN_HEADER]: shareToken?.token },
|
|
||||||
},
|
},
|
||||||
[modified, url, referrer, os, browser, device, country],
|
[modified, url, referrer, os, browser, device, country],
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -12,9 +12,7 @@ import useDateRange from 'hooks/useDateRange';
|
||||||
import useTimezone from 'hooks/useTimezone';
|
import useTimezone from 'hooks/useTimezone';
|
||||||
import usePageQuery from 'hooks/usePageQuery';
|
import usePageQuery from 'hooks/usePageQuery';
|
||||||
import { getDateArray, getDateLength, getDateRangeValues } from 'lib/date';
|
import { getDateArray, getDateLength, getDateRangeValues } from 'lib/date';
|
||||||
import useShareToken from 'hooks/useShareToken';
|
|
||||||
import useApi from 'hooks/useApi';
|
import useApi from 'hooks/useApi';
|
||||||
import { TOKEN_HEADER } from 'lib/constants';
|
|
||||||
import styles from './WebsiteChart.module.css';
|
import styles from './WebsiteChart.module.css';
|
||||||
|
|
||||||
export default function WebsiteChart({
|
export default function WebsiteChart({
|
||||||
|
|
@ -26,7 +24,6 @@ export default function WebsiteChart({
|
||||||
showChart = true,
|
showChart = true,
|
||||||
onDataLoad = () => {},
|
onDataLoad = () => {},
|
||||||
}) {
|
}) {
|
||||||
const shareToken = useShareToken();
|
|
||||||
const [dateRange, setDateRange] = useDateRange(websiteId);
|
const [dateRange, setDateRange] = useDateRange(websiteId);
|
||||||
const { startDate, endDate, unit, value, modified } = dateRange;
|
const { startDate, endDate, unit, value, modified } = dateRange;
|
||||||
const [timezone] = useTimezone();
|
const [timezone] = useTimezone();
|
||||||
|
|
@ -53,7 +50,6 @@ export default function WebsiteChart({
|
||||||
country,
|
country,
|
||||||
},
|
},
|
||||||
onDataLoad,
|
onDataLoad,
|
||||||
headers: { [TOKEN_HEADER]: shareToken?.token },
|
|
||||||
},
|
},
|
||||||
[modified, url, referrer, os, browser, device, country],
|
[modified, url, referrer, os, browser, device, country],
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,9 @@ export default function Dashboard() {
|
||||||
return (
|
return (
|
||||||
<Page>
|
<Page>
|
||||||
<PageHeader>
|
<PageHeader>
|
||||||
<div>Dashboard</div>
|
<div>
|
||||||
|
<FormattedMessage id="label.dashboard" defaultMessage="Dashboard" />
|
||||||
|
</div>
|
||||||
<DashboardSettingsButton />
|
<DashboardSettingsButton />
|
||||||
</PageHeader>
|
</PageHeader>
|
||||||
<WebsiteList websites={data} showCharts={showCharts} limit={max} />
|
<WebsiteList websites={data} showCharts={showCharts} limit={max} />
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ import useFetch from 'hooks/useFetch';
|
||||||
import useLocale from 'hooks/useLocale';
|
import useLocale from 'hooks/useLocale';
|
||||||
import useCountryNames from 'hooks/useCountryNames';
|
import useCountryNames from 'hooks/useCountryNames';
|
||||||
import { percentFilter } from 'lib/filters';
|
import { percentFilter } from 'lib/filters';
|
||||||
import { TOKEN_HEADER, REALTIME_RANGE, REALTIME_INTERVAL } from 'lib/constants';
|
import { SHARE_TOKEN_HEADER, REALTIME_RANGE, REALTIME_INTERVAL } from 'lib/constants';
|
||||||
import styles from './RealtimeDashboard.module.css';
|
import styles from './RealtimeDashboard.module.css';
|
||||||
|
|
||||||
function mergeData(state, data, time) {
|
function mergeData(state, data, time) {
|
||||||
|
|
@ -38,7 +38,7 @@ export default function RealtimeDashboard() {
|
||||||
params: { start_at: data?.timestamp },
|
params: { start_at: data?.timestamp },
|
||||||
disabled: !init?.websites?.length || !data,
|
disabled: !init?.websites?.length || !data,
|
||||||
interval: REALTIME_INTERVAL,
|
interval: REALTIME_INTERVAL,
|
||||||
headers: { [TOKEN_HEADER]: init?.token },
|
headers: { [SHARE_TOKEN_HEADER]: init?.token },
|
||||||
});
|
});
|
||||||
|
|
||||||
const renderCountryName = useCallback(
|
const renderCountryName = useCallback(
|
||||||
|
|
|
||||||
|
|
@ -20,8 +20,7 @@ import EventsTable from 'components/metrics/EventsTable';
|
||||||
import EventsChart from 'components/metrics/EventsChart';
|
import EventsChart from 'components/metrics/EventsChart';
|
||||||
import useFetch from 'hooks/useFetch';
|
import useFetch from 'hooks/useFetch';
|
||||||
import usePageQuery from 'hooks/usePageQuery';
|
import usePageQuery from 'hooks/usePageQuery';
|
||||||
import useShareToken from 'hooks/useShareToken';
|
import { DEFAULT_ANIMATION_DURATION } from 'lib/constants';
|
||||||
import { DEFAULT_ANIMATION_DURATION, TOKEN_HEADER } from 'lib/constants';
|
|
||||||
import styles from './WebsiteDetails.module.css';
|
import styles from './WebsiteDetails.module.css';
|
||||||
|
|
||||||
const views = {
|
const views = {
|
||||||
|
|
@ -36,10 +35,7 @@ const views = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function WebsiteDetails({ websiteId }) {
|
export default function WebsiteDetails({ websiteId }) {
|
||||||
const shareToken = useShareToken();
|
const { data } = useFetch(`/website/${websiteId}`);
|
||||||
const { data } = useFetch(`/website/${websiteId}`, {
|
|
||||||
headers: { [TOKEN_HEADER]: shareToken?.token },
|
|
||||||
});
|
|
||||||
const [chartLoaded, setChartLoaded] = useState(false);
|
const [chartLoaded, setChartLoaded] = useState(false);
|
||||||
const [countryData, setCountryData] = useState();
|
const [countryData, setCountryData] = useState();
|
||||||
const [eventsData, setEventsData] = useState();
|
const [eventsData, setEventsData] = useState();
|
||||||
|
|
|
||||||
|
|
@ -97,3 +97,6 @@ ALTER TABLE `session` ADD FOREIGN KEY (`website_id`) REFERENCES `website`(`websi
|
||||||
|
|
||||||
-- AddForeignKey
|
-- AddForeignKey
|
||||||
ALTER TABLE `website` ADD FOREIGN KEY (`user_id`) REFERENCES `account`(`user_id`) ON DELETE CASCADE ON UPDATE CASCADE;
|
ALTER TABLE `website` ADD FOREIGN KEY (`user_id`) REFERENCES `account`(`user_id`) ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- CreateAdminUser
|
||||||
|
INSERT INTO account (username, password, is_admin) values ('admin', '$2b$10$BUli0c.muyCW1ErNJc3jL.vFRFtFJWrT8/GcR4A.sUdCznaXiqFXa', true);
|
||||||
|
|
@ -127,3 +127,6 @@ ALTER TABLE "session" ADD FOREIGN KEY ("website_id") REFERENCES "website"("websi
|
||||||
|
|
||||||
-- AddForeignKey
|
-- AddForeignKey
|
||||||
ALTER TABLE "website" ADD FOREIGN KEY ("user_id") REFERENCES "account"("user_id") ON DELETE CASCADE ON UPDATE CASCADE;
|
ALTER TABLE "website" ADD FOREIGN KEY ("user_id") REFERENCES "account"("user_id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- CreateAdminUser
|
||||||
|
INSERT INTO account (username, password, is_admin) values ('admin', '$2b$10$BUli0c.muyCW1ErNJc3jL.vFRFtFJWrT8/GcR4A.sUdCznaXiqFXa', true);
|
||||||
|
|
@ -1,13 +1,18 @@
|
||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
import { get, post, put, del, getItem } from 'lib/web';
|
import { get, post, put, del, getItem } from 'lib/web';
|
||||||
import { AUTH_TOKEN } from 'lib/constants';
|
import { AUTH_TOKEN, SHARE_TOKEN_HEADER } from 'lib/constants';
|
||||||
|
import useStore from 'store/app';
|
||||||
|
|
||||||
function includeAuthToken(headers = {}) {
|
const selector = state => state.shareToken;
|
||||||
const authToken = getItem(AUTH_TOKEN);
|
|
||||||
|
|
||||||
|
function parseHeaders(headers = {}, { authToken, shareToken }) {
|
||||||
if (authToken) {
|
if (authToken) {
|
||||||
headers.Authorization = `Bearer ${authToken}`;
|
headers.authorization = `Bearer ${authToken}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shareToken) {
|
||||||
|
headers[SHARE_TOKEN_HEADER] = shareToken.token;
|
||||||
}
|
}
|
||||||
|
|
||||||
return headers;
|
return headers;
|
||||||
|
|
@ -15,32 +20,50 @@ function includeAuthToken(headers = {}) {
|
||||||
|
|
||||||
export default function useApi() {
|
export default function useApi() {
|
||||||
const { basePath } = useRouter();
|
const { basePath } = useRouter();
|
||||||
|
const authToken = getItem(AUTH_TOKEN);
|
||||||
|
const shareToken = useStore(selector);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
get: useCallback(
|
get: useCallback(
|
||||||
async (url, params, headers) => {
|
async (url, params, headers) => {
|
||||||
return get(`${basePath}/api${url}`, params, includeAuthToken(headers));
|
return get(
|
||||||
|
`${basePath}/api${url}`,
|
||||||
|
params,
|
||||||
|
parseHeaders(headers, { authToken, shareToken }),
|
||||||
|
);
|
||||||
},
|
},
|
||||||
[get],
|
[get],
|
||||||
),
|
),
|
||||||
|
|
||||||
post: useCallback(
|
post: useCallback(
|
||||||
async (url, params, headers) => {
|
async (url, params, headers) => {
|
||||||
return post(`${basePath}/api${url}`, params, includeAuthToken(headers));
|
return post(
|
||||||
|
`${basePath}/api${url}`,
|
||||||
|
params,
|
||||||
|
parseHeaders(headers, { authToken, shareToken }),
|
||||||
|
);
|
||||||
},
|
},
|
||||||
[post],
|
[post],
|
||||||
),
|
),
|
||||||
|
|
||||||
put: useCallback(
|
put: useCallback(
|
||||||
async (url, params, headers) => {
|
async (url, params, headers) => {
|
||||||
return put(`${basePath}/api${url}`, params, includeAuthToken(headers));
|
return put(
|
||||||
|
`${basePath}/api${url}`,
|
||||||
|
params,
|
||||||
|
parseHeaders(headers, { authToken, shareToken }),
|
||||||
|
);
|
||||||
},
|
},
|
||||||
[put],
|
[put],
|
||||||
),
|
),
|
||||||
|
|
||||||
del: useCallback(
|
del: useCallback(
|
||||||
async (url, params, headers) => {
|
async (url, params, headers) => {
|
||||||
return del(`${basePath}/api${url}`, params, includeAuthToken(headers));
|
return del(
|
||||||
|
`${basePath}/api${url}`,
|
||||||
|
params,
|
||||||
|
parseHeaders(headers, { authToken, shareToken }),
|
||||||
|
);
|
||||||
},
|
},
|
||||||
[del],
|
[del],
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -5,14 +5,14 @@ import useApi from './useApi';
|
||||||
export default function useFetch(url, options = {}, update = []) {
|
export default function useFetch(url, options = {}, update = []) {
|
||||||
const [response, setResponse] = useState();
|
const [response, setResponse] = useState();
|
||||||
const [error, setError] = useState();
|
const [error, setError] = useState();
|
||||||
const [loading, setLoadiing] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [count, setCount] = useState(0);
|
const [count, setCount] = useState(0);
|
||||||
const { get } = useApi();
|
const { get } = useApi();
|
||||||
const { params = {}, headers = {}, disabled, delay = 0, interval, onDataLoad } = options;
|
const { params = {}, headers = {}, disabled, delay = 0, interval, onDataLoad } = options;
|
||||||
|
|
||||||
async function loadData(params) {
|
async function loadData(params) {
|
||||||
try {
|
try {
|
||||||
setLoadiing(true);
|
setLoading(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
const time = performance.now();
|
const time = performance.now();
|
||||||
|
|
||||||
|
|
@ -32,7 +32,7 @@ export default function useFetch(url, options = {}, update = []) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
setError(e);
|
setError(e);
|
||||||
} finally {
|
} finally {
|
||||||
setLoadiing(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,21 +0,0 @@
|
||||||
import { useEffect, useCallback } from 'react';
|
|
||||||
import useStore, { checkVersion } from 'store/version';
|
|
||||||
import { VERSION_CHECK } from 'lib/constants';
|
|
||||||
import { getItem, setItem } from 'lib/web';
|
|
||||||
|
|
||||||
export default function useVersion(check) {
|
|
||||||
const versions = useStore();
|
|
||||||
const checked = versions.latest === getItem(VERSION_CHECK)?.version;
|
|
||||||
|
|
||||||
const updateCheck = useCallback(() => {
|
|
||||||
setItem(VERSION_CHECK, { version: versions.latest, time: Date.now() });
|
|
||||||
}, [versions]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (check && !versions.latest) {
|
|
||||||
checkVersion();
|
|
||||||
}
|
|
||||||
}, [versions, check]);
|
|
||||||
|
|
||||||
return { ...versions, checked, updateCheck };
|
|
||||||
}
|
|
||||||
|
|
@ -4,6 +4,7 @@
|
||||||
"label.administrator",
|
"label.administrator",
|
||||||
"label.name",
|
"label.name",
|
||||||
"label.domain",
|
"label.domain",
|
||||||
|
"label.theme",
|
||||||
"metrics.device.desktop",
|
"metrics.device.desktop",
|
||||||
"metrics.device.laptop",
|
"metrics.device.laptop",
|
||||||
"metrics.device.tablet",
|
"metrics.device.tablet",
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,7 @@
|
||||||
"label.enable-share-url": "Freigabe-URL aktivieren",
|
"label.enable-share-url": "Freigabe-URL aktivieren",
|
||||||
"label.invalid": "Ungültig",
|
"label.invalid": "Ungültig",
|
||||||
"label.invalid-domain": "Ungültige Domain",
|
"label.invalid-domain": "Ungültige Domain",
|
||||||
"label.language": "Language",
|
"label.language": "Sprache",
|
||||||
"label.last-days": "Letzten {x} Tage",
|
"label.last-days": "Letzten {x} Tage",
|
||||||
"label.last-hours": "Letzten {x} Stunden",
|
"label.last-hours": "Letzten {x} Stunden",
|
||||||
"label.logged-in-as": "Angemeldet als {username}",
|
"label.logged-in-as": "Angemeldet als {username}",
|
||||||
|
|
@ -51,7 +51,7 @@
|
||||||
"label.settings": "Einstellungen",
|
"label.settings": "Einstellungen",
|
||||||
"label.share-url": "Freigabe-URL",
|
"label.share-url": "Freigabe-URL",
|
||||||
"label.single-day": "Ein Tag",
|
"label.single-day": "Ein Tag",
|
||||||
"label.theme": "Theme",
|
"label.theme": "Thema",
|
||||||
"label.this-month": "Diesen Monat",
|
"label.this-month": "Diesen Monat",
|
||||||
"label.this-week": "Diese Woche",
|
"label.this-week": "Diese Woche",
|
||||||
"label.this-year": "Dieses Jahr",
|
"label.this-year": "Dieses Jahr",
|
||||||
|
|
@ -92,7 +92,7 @@
|
||||||
"metrics.countries": "Länder",
|
"metrics.countries": "Länder",
|
||||||
"metrics.device.desktop": "Desktop",
|
"metrics.device.desktop": "Desktop",
|
||||||
"metrics.device.laptop": "Laptop",
|
"metrics.device.laptop": "Laptop",
|
||||||
"metrics.device.mobile": "Mobiltelefon",
|
"metrics.device.mobile": "Handy",
|
||||||
"metrics.device.tablet": "Tablet",
|
"metrics.device.tablet": "Tablet",
|
||||||
"metrics.devices": "Geräte",
|
"metrics.devices": "Geräte",
|
||||||
"metrics.events": "Ereignisse",
|
"metrics.events": "Ereignisse",
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@
|
||||||
"label.administrator": "مدیر",
|
"label.administrator": "مدیر",
|
||||||
"label.all": "همه",
|
"label.all": "همه",
|
||||||
"label.all-events": "همهی رویدادها",
|
"label.all-events": "همهی رویدادها",
|
||||||
"label.all-time": "All time",
|
"label.all-time": "همه زمان",
|
||||||
"label.all-websites": "همهی وبسایتها",
|
"label.all-websites": "همهی وبسایتها",
|
||||||
"label.back": "برگشت",
|
"label.back": "برگشت",
|
||||||
"label.cancel": "انصراف",
|
"label.cancel": "انصراف",
|
||||||
|
|
@ -28,16 +28,16 @@
|
||||||
"label.enable-share-url": "فعال کردن اشتراک گذاری URL",
|
"label.enable-share-url": "فعال کردن اشتراک گذاری URL",
|
||||||
"label.invalid": "نامعتبر",
|
"label.invalid": "نامعتبر",
|
||||||
"label.invalid-domain": "دامنهی نامعتبر",
|
"label.invalid-domain": "دامنهی نامعتبر",
|
||||||
"label.language": "Language",
|
"label.language": "زبان",
|
||||||
"label.last-days": "لیست {x} روز",
|
"label.last-days": "لیست {x} روز گذشته",
|
||||||
"label.last-hours": "لیست {x} ساعت",
|
"label.last-hours": "لیست {x} ساعت گذشته",
|
||||||
"label.logged-in-as": "وارد شده به عنوان {username}",
|
"label.logged-in-as": "وارد شده به عنوان {username}",
|
||||||
"label.login": "ورود",
|
"label.login": "ورود",
|
||||||
"label.logout": "خروج",
|
"label.logout": "خروج",
|
||||||
"label.more": "بیشتر",
|
"label.more": "بیشتر",
|
||||||
"label.name": "نام",
|
"label.name": "نام",
|
||||||
"label.new-password": "رمز جدید",
|
"label.new-password": "رمز جدید",
|
||||||
"label.owner": "Owner",
|
"label.owner": "ایجاد شده توسط",
|
||||||
"label.password": "رمز",
|
"label.password": "رمز",
|
||||||
"label.passwords-dont-match": "رمزها یکسان نیستند",
|
"label.passwords-dont-match": "رمزها یکسان نیستند",
|
||||||
"label.profile": "پروفایل",
|
"label.profile": "پروفایل",
|
||||||
|
|
@ -46,12 +46,12 @@
|
||||||
"label.refresh": "بهروزرسانی",
|
"label.refresh": "بهروزرسانی",
|
||||||
"label.required": "ضروری",
|
"label.required": "ضروری",
|
||||||
"label.reset": "بازنشانی",
|
"label.reset": "بازنشانی",
|
||||||
"label.reset-website": "Reset statistics",
|
"label.reset-website": "بازنشانی آمار",
|
||||||
"label.save": "ذخیره",
|
"label.save": "ذخیره",
|
||||||
"label.settings": "تنظیمات",
|
"label.settings": "تنظیمات",
|
||||||
"label.share-url": "به اشتراک گذاری URL",
|
"label.share-url": "به اشتراک گذاری URL",
|
||||||
"label.single-day": "یک روز",
|
"label.single-day": "یک روز",
|
||||||
"label.theme": "Theme",
|
"label.theme": "تم",
|
||||||
"label.this-month": "این ماه",
|
"label.this-month": "این ماه",
|
||||||
"label.this-week": "این هفته",
|
"label.this-week": "این هفته",
|
||||||
"label.this-year": "امسال",
|
"label.this-year": "امسال",
|
||||||
|
|
@ -64,7 +64,7 @@
|
||||||
"label.websites": "وبسایتها",
|
"label.websites": "وبسایتها",
|
||||||
"message.active-users": "{x} هم اکنون {x, plural, one {یک} other {از میان}}",
|
"message.active-users": "{x} هم اکنون {x, plural, one {یک} other {از میان}}",
|
||||||
"message.confirm-delete": "آیا مطمئن هستید میخواهید {target} را حذف کنید?",
|
"message.confirm-delete": "آیا مطمئن هستید میخواهید {target} را حذف کنید?",
|
||||||
"message.confirm-reset": "Are your sure you want to reset {target}'s statistics?",
|
"message.confirm-reset": "آیا از بازنشانی آمار {target} مطمئن هستید?",
|
||||||
"message.copied": "کپی شد!",
|
"message.copied": "کپی شد!",
|
||||||
"message.delete-warning": "همهی دادههای مرتبط هم حذف خواهد شد.",
|
"message.delete-warning": "همهی دادههای مرتبط هم حذف خواهد شد.",
|
||||||
"message.failure": "مشکلی پیش آمده است.",
|
"message.failure": "مشکلی پیش آمده است.",
|
||||||
|
|
@ -78,7 +78,7 @@
|
||||||
"message.no-websites-configured": "شما هیچ وبسایتی را پیکربندی نکردهاید.",
|
"message.no-websites-configured": "شما هیچ وبسایتی را پیکربندی نکردهاید.",
|
||||||
"message.page-not-found": "صفحه یافت نشد.",
|
"message.page-not-found": "صفحه یافت نشد.",
|
||||||
"message.powered-by": "قدرت گرفته توسط {name}",
|
"message.powered-by": "قدرت گرفته توسط {name}",
|
||||||
"message.reset-warning": "All statistics for this website will be deleted, but your tracking code will remain intact.",
|
"message.reset-warning": "تمامی آمارهای این وبسایت حذف خواهد شد اما tracking code بدون تغییر باقی میماند.",
|
||||||
"message.save-success": "با موفقیت ذخیره شد.",
|
"message.save-success": "با موفقیت ذخیره شد.",
|
||||||
"message.share-url": "این URL به اشتراک گذاشته شده عمومی برای {target} است.",
|
"message.share-url": "این URL به اشتراک گذاشته شده عمومی برای {target} است.",
|
||||||
"message.toggle-charts": "Toggle charts",
|
"message.toggle-charts": "Toggle charts",
|
||||||
|
|
@ -99,7 +99,7 @@
|
||||||
"metrics.filter.combined": "ترکیب شده",
|
"metrics.filter.combined": "ترکیب شده",
|
||||||
"metrics.filter.domain-only": "فقط دامنه",
|
"metrics.filter.domain-only": "فقط دامنه",
|
||||||
"metrics.filter.raw": "خام",
|
"metrics.filter.raw": "خام",
|
||||||
"metrics.languages": "Languages",
|
"metrics.languages": "زبانها",
|
||||||
"metrics.operating-systems": "سیستمعاملها",
|
"metrics.operating-systems": "سیستمعاملها",
|
||||||
"metrics.page-views": "بازدید صفحه",
|
"metrics.page-views": "بازدید صفحه",
|
||||||
"metrics.pages": "صفحهها",
|
"metrics.pages": "صفحهها",
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@
|
||||||
"label.administrator": "Administrateur",
|
"label.administrator": "Administrateur",
|
||||||
"label.all": "Tout",
|
"label.all": "Tout",
|
||||||
"label.all-events": "Tous les événements",
|
"label.all-events": "Tous les événements",
|
||||||
"label.all-time": "Toutes périodes",
|
"label.all-time": "Toutes les données",
|
||||||
"label.all-websites": "Tous les sites web",
|
"label.all-websites": "Tous les sites web",
|
||||||
"label.back": "Retour",
|
"label.back": "Retour",
|
||||||
"label.cancel": "Annuler",
|
"label.cancel": "Annuler",
|
||||||
|
|
@ -13,10 +13,10 @@
|
||||||
"label.confirm-password": "Confirmation du mot de passe",
|
"label.confirm-password": "Confirmation du mot de passe",
|
||||||
"label.copy-to-clipboard": "Copier dans le presse papier",
|
"label.copy-to-clipboard": "Copier dans le presse papier",
|
||||||
"label.current-password": "Mot de passe actuel",
|
"label.current-password": "Mot de passe actuel",
|
||||||
"label.custom-range": "Intervalle personnalisé",
|
"label.custom-range": "Période personnalisée",
|
||||||
"label.dashboard": "Tableau de bord",
|
"label.dashboard": "Tableau de bord",
|
||||||
"label.date-range": "Intervalle",
|
"label.date-range": "Période",
|
||||||
"label.default-date-range": "Intervalle par défaut",
|
"label.default-date-range": "Période par défaut",
|
||||||
"label.delete": "Supprimer",
|
"label.delete": "Supprimer",
|
||||||
"label.delete-account": "Supprimer le compte",
|
"label.delete-account": "Supprimer le compte",
|
||||||
"label.delete-website": "Supprimer le site",
|
"label.delete-website": "Supprimer le site",
|
||||||
|
|
@ -25,7 +25,7 @@
|
||||||
"label.edit": "Modifier",
|
"label.edit": "Modifier",
|
||||||
"label.edit-account": "Modifier le compte",
|
"label.edit-account": "Modifier le compte",
|
||||||
"label.edit-website": "Modifier le site",
|
"label.edit-website": "Modifier le site",
|
||||||
"label.enable-share-url": "Activer le partage d'URL",
|
"label.enable-share-url": "Activer l'URL de partage",
|
||||||
"label.invalid": "Invalide",
|
"label.invalid": "Invalide",
|
||||||
"label.invalid-domain": "Domaine invalide",
|
"label.invalid-domain": "Domaine invalide",
|
||||||
"label.language": "Langage",
|
"label.language": "Langage",
|
||||||
|
|
@ -68,10 +68,10 @@
|
||||||
"message.copied": "Copié !",
|
"message.copied": "Copié !",
|
||||||
"message.delete-warning": "Toutes les données associées seront également supprimées.",
|
"message.delete-warning": "Toutes les données associées seront également supprimées.",
|
||||||
"message.failure": "Un problème est survenu.",
|
"message.failure": "Un problème est survenu.",
|
||||||
"message.get-share-url": "Obtenez l'URL de partage",
|
"message.get-share-url": "Obtenir l'URL de partage",
|
||||||
"message.get-tracking-code": "Obtenez le code de suivi",
|
"message.get-tracking-code": "Obtenir le code de suivi",
|
||||||
"message.go-to-settings": "Aller aux paramètres",
|
"message.go-to-settings": "Aller aux paramètres",
|
||||||
"message.incorrect-username-password": "nom d'utilisateurs/mot de passe incorrect.",
|
"message.incorrect-username-password": "Nom d'utilisateur/Mot de passe incorrect.",
|
||||||
"message.log.visitor": "Visiteur de {country} utilisant {browser} sur {os} {device}",
|
"message.log.visitor": "Visiteur de {country} utilisant {browser} sur {os} {device}",
|
||||||
"message.new-version-available": "Une nouvelle version de umami {version} est disponible !",
|
"message.new-version-available": "Une nouvelle version de umami {version} est disponible !",
|
||||||
"message.no-data-available": "Pas de données disponibles.",
|
"message.no-data-available": "Pas de données disponibles.",
|
||||||
|
|
@ -81,7 +81,7 @@
|
||||||
"message.reset-warning": "Toutes les statistiques pour ce site seront supprimés, mais votre code de suivi restera intact.",
|
"message.reset-warning": "Toutes les statistiques pour ce site seront supprimés, mais votre code de suivi restera intact.",
|
||||||
"message.save-success": "Enregistré avec succès.",
|
"message.save-success": "Enregistré avec succès.",
|
||||||
"message.share-url": "Ceci est l'URL partagée pour {target}.",
|
"message.share-url": "Ceci est l'URL partagée pour {target}.",
|
||||||
"message.toggle-charts": "Changer les graphiques",
|
"message.toggle-charts": "Afficher/Masquer les graphiques",
|
||||||
"message.track-stats": "Pour suivre les statistiques de {target}, placez le code suivant dans la section {head} de votre site Web.",
|
"message.track-stats": "Pour suivre les statistiques de {target}, placez le code suivant dans la section {head} de votre site Web.",
|
||||||
"message.type-delete": "Tapez {delete} dans la case ci-dessous pour confirmer.",
|
"message.type-delete": "Tapez {delete} dans la case ci-dessous pour confirmer.",
|
||||||
"message.type-reset": "Tapez {reset} dans la case ci-dessous pour confirmer.",
|
"message.type-reset": "Tapez {reset} dans la case ci-dessous pour confirmer.",
|
||||||
|
|
@ -98,12 +98,12 @@
|
||||||
"metrics.events": "Événements",
|
"metrics.events": "Événements",
|
||||||
"metrics.filter.combined": "Combiné",
|
"metrics.filter.combined": "Combiné",
|
||||||
"metrics.filter.domain-only": "Domaine uniquement",
|
"metrics.filter.domain-only": "Domaine uniquement",
|
||||||
"metrics.filter.raw": "Brute",
|
"metrics.filter.raw": "Brut",
|
||||||
"metrics.languages": "Langages",
|
"metrics.languages": "Langages",
|
||||||
"metrics.operating-systems": "Systèmes d'exploitation",
|
"metrics.operating-systems": "Systèmes d'exploitation",
|
||||||
"metrics.page-views": "Pages vues",
|
"metrics.page-views": "Pages vues",
|
||||||
"metrics.pages": "Pages",
|
"metrics.pages": "Pages",
|
||||||
"metrics.referrers": "URL Référentes",
|
"metrics.referrers": "Sources",
|
||||||
"metrics.unique-visitors": "Visiteurs uniques",
|
"metrics.unique-visitors": "Visiteurs uniques",
|
||||||
"metrics.views": "Vues",
|
"metrics.views": "Vues",
|
||||||
"metrics.visitors": "Visiteurs"
|
"metrics.visitors": "Visiteurs"
|
||||||
|
|
|
||||||
110
lang/ga-ES.json
Normal file
110
lang/ga-ES.json
Normal file
|
|
@ -0,0 +1,110 @@
|
||||||
|
{
|
||||||
|
"label.accounts": "Contas",
|
||||||
|
"label.add-account": "Engadir conta",
|
||||||
|
"label.add-website": "Engadir sitio web",
|
||||||
|
"label.administrator": "Administradora",
|
||||||
|
"label.all": "Todo",
|
||||||
|
"label.all-events": "Tódolos eventos",
|
||||||
|
"label.all-time": "Sempre",
|
||||||
|
"label.all-websites": "Tódolos sitios web",
|
||||||
|
"label.back": "Atrás",
|
||||||
|
"label.cancel": "Cancelar",
|
||||||
|
"label.change-password": "Mudar contrasinal",
|
||||||
|
"label.confirm-password": "Confirmar contrasinal",
|
||||||
|
"label.copy-to-clipboard": "Copiar ao portapapeis",
|
||||||
|
"label.current-password": "Contrasinal actual",
|
||||||
|
"label.custom-range": "Rango personalizado",
|
||||||
|
"label.dashboard": "Taboleiro",
|
||||||
|
"label.date-range": "Rango temporal",
|
||||||
|
"label.default-date-range": "Rango temporal por defecto",
|
||||||
|
"label.delete": "Eliminar",
|
||||||
|
"label.delete-account": "Eliminar conta",
|
||||||
|
"label.delete-website": "Eliminar sitio web",
|
||||||
|
"label.dismiss": "Desbotar",
|
||||||
|
"label.domain": "Dominio",
|
||||||
|
"label.edit": "Editar",
|
||||||
|
"label.edit-account": "Editar conta",
|
||||||
|
"label.edit-website": "Editar sitio web",
|
||||||
|
"label.enable-share-url": "Activar URL de compartición",
|
||||||
|
"label.invalid": "Non válido",
|
||||||
|
"label.invalid-domain": "Dominio non válido",
|
||||||
|
"label.language": "Idioma",
|
||||||
|
"label.last-days": "Últimos {x} días",
|
||||||
|
"label.last-hours": "Últimas {x} horas",
|
||||||
|
"label.logged-in-as": "Sesión de {username}",
|
||||||
|
"label.login": "Acceder",
|
||||||
|
"label.logout": "Pechar sesión",
|
||||||
|
"label.more": "Máis",
|
||||||
|
"label.name": "Nome",
|
||||||
|
"label.new-password": "Novo contrasinal",
|
||||||
|
"label.owner": "Dona",
|
||||||
|
"label.password": "Contrasinal",
|
||||||
|
"label.passwords-dont-match": "Non concordan os contrasinais",
|
||||||
|
"label.profile": "Perfil",
|
||||||
|
"label.realtime": "Agora mesmo",
|
||||||
|
"label.realtime-logs": "Rexistro neste intre",
|
||||||
|
"label.refresh": "Actualizar",
|
||||||
|
"label.required": "Requerido",
|
||||||
|
"label.reset": "Restablecer",
|
||||||
|
"label.reset-website": "Restablecer estatísticas",
|
||||||
|
"label.save": "Gardar",
|
||||||
|
"label.settings": "Axustes",
|
||||||
|
"label.share-url": "Compartir URL",
|
||||||
|
"label.single-day": "Un só día",
|
||||||
|
"label.theme": "Decorado",
|
||||||
|
"label.this-month": "Este mes",
|
||||||
|
"label.this-week": "Esta semana",
|
||||||
|
"label.this-year": "Este ano",
|
||||||
|
"label.timezone": "Zona horaria",
|
||||||
|
"label.today": "Hoxe",
|
||||||
|
"label.tracking-code": "Código de seguimento",
|
||||||
|
"label.unknown": "Descoñecido",
|
||||||
|
"label.username": "Identificador",
|
||||||
|
"label.view-details": "Ver detalles",
|
||||||
|
"label.websites": "Sitios web",
|
||||||
|
"message.active-users": "{x} actual {x, plural, one {visitante} other {visitantes}}",
|
||||||
|
"message.confirm-delete": "Tes a certeza de querer eliminar {target}?",
|
||||||
|
"message.confirm-reset": "Tes a certeza de querer restablecer as estatísticas de {target}?",
|
||||||
|
"message.copied": "Copiado!",
|
||||||
|
"message.delete-warning": "Tamén serán borrados tódolos datos asociados.",
|
||||||
|
"message.failure": "Houbo un fallo.",
|
||||||
|
"message.get-share-url": "Obter URL de compartición",
|
||||||
|
"message.get-tracking-code": "Obter código de seguimento",
|
||||||
|
"message.go-to-settings": "Ir aos axustes",
|
||||||
|
"message.incorrect-username-password": "Credenciais incorrectas.",
|
||||||
|
"message.log.visitor": "Visitante desde {country} usando {browser} en {os} {device}",
|
||||||
|
"message.new-version-available": "A nova versión {version} de umami está dispoñible!",
|
||||||
|
"message.no-data-available": "Sen datos dispoñibles.",
|
||||||
|
"message.no-websites-configured": "Non tes sitios web configurados.",
|
||||||
|
"message.page-not-found": "Páxina non atopada.",
|
||||||
|
"message.powered-by": "Funciona grazas a {name}",
|
||||||
|
"message.reset-warning": "Vanse eliminar tódalas estatísticas deste sitio web, pero o código de seguimento permanecerá sen cambios.",
|
||||||
|
"message.save-success": "Gardouse correctamente.",
|
||||||
|
"message.share-url": "Este é o URL da compartición pública de {target}.",
|
||||||
|
"message.toggle-charts": "Activación das gráficas",
|
||||||
|
"message.track-stats": "Para crear estatísticas de {target}, pon este código na sección {head} do teu sitio web.",
|
||||||
|
"message.type-delete": "Escribe {delete} na caixa inferior para confirmar.",
|
||||||
|
"message.type-reset": "Escribe {reset} na caixa inferior para confirmar.",
|
||||||
|
"metrics.actions": "Accións",
|
||||||
|
"metrics.average-visit-time": "Tempo medio de visita",
|
||||||
|
"metrics.bounce-rate": "Proporción de rebote",
|
||||||
|
"metrics.browsers": "Navegadores",
|
||||||
|
"metrics.countries": "Países",
|
||||||
|
"metrics.device.desktop": "Escritorio",
|
||||||
|
"metrics.device.laptop": "Portátil",
|
||||||
|
"metrics.device.mobile": "Móbil",
|
||||||
|
"metrics.device.tablet": "Tableta",
|
||||||
|
"metrics.devices": "Dispositivos",
|
||||||
|
"metrics.events": "Eventos",
|
||||||
|
"metrics.filter.combined": "Combinado",
|
||||||
|
"metrics.filter.domain-only": "Só dominio",
|
||||||
|
"metrics.filter.raw": "Raw",
|
||||||
|
"metrics.languages": "Idiomas",
|
||||||
|
"metrics.operating-systems": "Sistemas operativos",
|
||||||
|
"metrics.page-views": "Vistas de páxinas",
|
||||||
|
"metrics.pages": "Páxinas",
|
||||||
|
"metrics.referrers": "Orixes",
|
||||||
|
"metrics.unique-visitors": "Visitas únicas",
|
||||||
|
"metrics.views": "Visualizacións",
|
||||||
|
"metrics.visitors": "Visitantes"
|
||||||
|
}
|
||||||
|
|
@ -12,7 +12,7 @@
|
||||||
"label.change-password": "Modifica password",
|
"label.change-password": "Modifica password",
|
||||||
"label.confirm-password": "Conferma password",
|
"label.confirm-password": "Conferma password",
|
||||||
"label.copy-to-clipboard": "Copia",
|
"label.copy-to-clipboard": "Copia",
|
||||||
"label.current-password": "Password corrente",
|
"label.current-password": "Password attuale",
|
||||||
"label.custom-range": "Personalizzato",
|
"label.custom-range": "Personalizzato",
|
||||||
"label.dashboard": "Pannello di Controllo",
|
"label.dashboard": "Pannello di Controllo",
|
||||||
"label.date-range": "Periodo",
|
"label.date-range": "Periodo",
|
||||||
|
|
@ -28,7 +28,7 @@
|
||||||
"label.enable-share-url": "Abilita URL di condivisione",
|
"label.enable-share-url": "Abilita URL di condivisione",
|
||||||
"label.invalid": "Non valido",
|
"label.invalid": "Non valido",
|
||||||
"label.invalid-domain": "Dominio non valido",
|
"label.invalid-domain": "Dominio non valido",
|
||||||
"label.language": "Language",
|
"label.language": "Lingua",
|
||||||
"label.last-days": "Ultimi {x} giorni",
|
"label.last-days": "Ultimi {x} giorni",
|
||||||
"label.last-hours": "Ultime {x} ore",
|
"label.last-hours": "Ultime {x} ore",
|
||||||
"label.logged-in-as": "Ciao {username}",
|
"label.logged-in-as": "Ciao {username}",
|
||||||
|
|
@ -51,7 +51,7 @@
|
||||||
"label.settings": "Impostazioni",
|
"label.settings": "Impostazioni",
|
||||||
"label.share-url": "Condividi link",
|
"label.share-url": "Condividi link",
|
||||||
"label.single-day": "Singolo giorno",
|
"label.single-day": "Singolo giorno",
|
||||||
"label.theme": "Theme",
|
"label.theme": "Tema",
|
||||||
"label.this-month": "Questo mese",
|
"label.this-month": "Questo mese",
|
||||||
"label.this-week": "Questa settimana",
|
"label.this-week": "Questa settimana",
|
||||||
"label.this-year": "Quest'anno",
|
"label.this-year": "Quest'anno",
|
||||||
|
|
@ -64,7 +64,7 @@
|
||||||
"label.websites": "Siti web",
|
"label.websites": "Siti web",
|
||||||
"message.active-users": "{x} {x, plural, one {visitatore} other {visitatori}} online",
|
"message.active-users": "{x} {x, plural, one {visitatore} other {visitatori}} online",
|
||||||
"message.confirm-delete": "Sei sicuro di voler eliminare {target}?",
|
"message.confirm-delete": "Sei sicuro di voler eliminare {target}?",
|
||||||
"message.confirm-reset": "Are your sure you want to reset {target}'s statistics?",
|
"message.confirm-reset": "Sei sicuro di voler azzerare le statistiche di {target}?",
|
||||||
"message.copied": "Copiato!",
|
"message.copied": "Copiato!",
|
||||||
"message.delete-warning": "Saranno eliminati anche tutti i dati associati.",
|
"message.delete-warning": "Saranno eliminati anche tutti i dati associati.",
|
||||||
"message.failure": "Si è verificato un errore.",
|
"message.failure": "Si è verificato un errore.",
|
||||||
|
|
@ -73,7 +73,7 @@
|
||||||
"message.go-to-settings": "Vai alle impostazioni",
|
"message.go-to-settings": "Vai alle impostazioni",
|
||||||
"message.incorrect-username-password": "Username o password non corretti.",
|
"message.incorrect-username-password": "Username o password non corretti.",
|
||||||
"message.log.visitor": "Utenti da {country} tramite {browser} su {os} {device}",
|
"message.log.visitor": "Utenti da {country} tramite {browser} su {os} {device}",
|
||||||
"message.new-version-available": "Una nuova versione umami {version} è disponibile!",
|
"message.new-version-available": "Una nuova versione {version} di umami è disponibile!",
|
||||||
"message.no-data-available": "Nessun dato disponibile.",
|
"message.no-data-available": "Nessun dato disponibile.",
|
||||||
"message.no-websites-configured": "Non hai ancora configurato alcun sito.",
|
"message.no-websites-configured": "Non hai ancora configurato alcun sito.",
|
||||||
"message.page-not-found": "Pagina non trovata",
|
"message.page-not-found": "Pagina non trovata",
|
||||||
|
|
@ -103,7 +103,7 @@
|
||||||
"metrics.operating-systems": "Sistemi operativi",
|
"metrics.operating-systems": "Sistemi operativi",
|
||||||
"metrics.page-views": "Visualizzazioni di pagina",
|
"metrics.page-views": "Visualizzazioni di pagina",
|
||||||
"metrics.pages": "Pagine",
|
"metrics.pages": "Pagine",
|
||||||
"metrics.referrers": "Referr",
|
"metrics.referrers": "Referrers",
|
||||||
"metrics.unique-visitors": "Visitatori unici",
|
"metrics.unique-visitors": "Visitatori unici",
|
||||||
"metrics.views": "Visualizzazioni",
|
"metrics.views": "Visualizzazioni",
|
||||||
"metrics.visitors": "Visitatori"
|
"metrics.visitors": "Visitatori"
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@
|
||||||
"label.administrator": "Админ",
|
"label.administrator": "Админ",
|
||||||
"label.all": "Бүх",
|
"label.all": "Бүх",
|
||||||
"label.all-events": "Бүх үйл явдал",
|
"label.all-events": "Бүх үйл явдал",
|
||||||
"label.all-time": "All time",
|
"label.all-time": "Бүх цаг үеийн",
|
||||||
"label.all-websites": "Бүх вебүүд",
|
"label.all-websites": "Бүх вебүүд",
|
||||||
"label.back": "Буцах",
|
"label.back": "Буцах",
|
||||||
"label.cancel": "Цуцлах",
|
"label.cancel": "Цуцлах",
|
||||||
|
|
@ -28,7 +28,7 @@
|
||||||
"label.enable-share-url": "Хуваалцах холбоос идэвхжүүлэх",
|
"label.enable-share-url": "Хуваалцах холбоос идэвхжүүлэх",
|
||||||
"label.invalid": "Буруу",
|
"label.invalid": "Буруу",
|
||||||
"label.invalid-domain": "Буруу домэйн",
|
"label.invalid-domain": "Буруу домэйн",
|
||||||
"label.language": "Language",
|
"label.language": "Хэл",
|
||||||
"label.last-days": "Сүүлийн {x} хоног",
|
"label.last-days": "Сүүлийн {x} хоног",
|
||||||
"label.last-hours": "Сүүлийн {x} цаг",
|
"label.last-hours": "Сүүлийн {x} цаг",
|
||||||
"label.logged-in-as": "{username}-р нэвтэрсэн",
|
"label.logged-in-as": "{username}-р нэвтэрсэн",
|
||||||
|
|
@ -37,7 +37,7 @@
|
||||||
"label.more": "Цааш",
|
"label.more": "Цааш",
|
||||||
"label.name": "Нэр",
|
"label.name": "Нэр",
|
||||||
"label.new-password": "Шинэ нууц үг",
|
"label.new-password": "Шинэ нууц үг",
|
||||||
"label.owner": "Owner",
|
"label.owner": "Эзэмшигч",
|
||||||
"label.password": "Нууц үг",
|
"label.password": "Нууц үг",
|
||||||
"label.passwords-dont-match": "Нууц үг тохирохгүй байна",
|
"label.passwords-dont-match": "Нууц үг тохирохгүй байна",
|
||||||
"label.profile": "Бүртгэл",
|
"label.profile": "Бүртгэл",
|
||||||
|
|
@ -46,12 +46,12 @@
|
||||||
"label.refresh": "Сэргээх",
|
"label.refresh": "Сэргээх",
|
||||||
"label.required": "Шаардлагатай",
|
"label.required": "Шаардлагатай",
|
||||||
"label.reset": "Хуучин хэвд нь оруулах",
|
"label.reset": "Хуучин хэвд нь оруулах",
|
||||||
"label.reset-website": "Reset statistics",
|
"label.reset-website": "Тоон үзүүлэлтийг дахин эхлүүлэх",
|
||||||
"label.save": "Хадгалах",
|
"label.save": "Хадгалах",
|
||||||
"label.settings": "Тохиргоо",
|
"label.settings": "Тохиргоо",
|
||||||
"label.share-url": "Хуваалцах холбоос",
|
"label.share-url": "Хуваалцах холбоос",
|
||||||
"label.single-day": "Нэг өдөр",
|
"label.single-day": "Нэг өдөр",
|
||||||
"label.theme": "Theme",
|
"label.theme": "Загвар",
|
||||||
"label.this-month": "Энэ сар",
|
"label.this-month": "Энэ сар",
|
||||||
"label.this-week": "Энэ долоо хоног",
|
"label.this-week": "Энэ долоо хоног",
|
||||||
"label.this-year": "Энэ жил",
|
"label.this-year": "Энэ жил",
|
||||||
|
|
@ -78,10 +78,10 @@
|
||||||
"message.no-websites-configured": "Та ямар нэгэн веб тохируулаагүй байна.",
|
"message.no-websites-configured": "Та ямар нэгэн веб тохируулаагүй байна.",
|
||||||
"message.page-not-found": "Хуудас олдсонгүй.",
|
"message.page-not-found": "Хуудас олдсонгүй.",
|
||||||
"message.powered-by": "{name} дээр суурилсан",
|
"message.powered-by": "{name} дээр суурилсан",
|
||||||
"message.reset-warning": "All statistics for this website will be deleted, but your tracking code will remain intact.",
|
"message.reset-warning": "Энэ вебийн бүх тоон үзүүлэлтүүдийг устгах болно. Гэхдээ мөрдөх код хэвэндээ үлдэнэ.",
|
||||||
"message.save-success": "Амжилттай хадгаллаа.",
|
"message.save-success": "Амжилттай хадгаллаа.",
|
||||||
"message.share-url": "{target}-г нийтэд хуваалцах холбоос.",
|
"message.share-url": "{target}-г нийтэд хуваалцах холбоос.",
|
||||||
"message.toggle-charts": "Toggle charts",
|
"message.toggle-charts": "Графикийг харуулах/нуух",
|
||||||
"message.track-stats": "{target} вебийн статистикийг бүртгэхийн тулд доорх кодыг вебийнхээ {head} хэсэгт байрлуулна уу.",
|
"message.track-stats": "{target} вебийн статистикийг бүртгэхийн тулд доорх кодыг вебийнхээ {head} хэсэгт байрлуулна уу.",
|
||||||
"message.type-delete": "Доорх хэсэгт {delete} гэж бичиж баталгаажуулна уу.",
|
"message.type-delete": "Доорх хэсэгт {delete} гэж бичиж баталгаажуулна уу.",
|
||||||
"message.type-reset": "Доорх хэсэгт {reset} гэж бичиж баталгаажуулна уу.",
|
"message.type-reset": "Доорх хэсэгт {reset} гэж бичиж баталгаажуулна уу.",
|
||||||
|
|
@ -99,7 +99,7 @@
|
||||||
"metrics.filter.combined": "Нэгтгэсэн",
|
"metrics.filter.combined": "Нэгтгэсэн",
|
||||||
"metrics.filter.domain-only": "Зөвхөн домэйн",
|
"metrics.filter.domain-only": "Зөвхөн домэйн",
|
||||||
"metrics.filter.raw": "Түүхий",
|
"metrics.filter.raw": "Түүхий",
|
||||||
"metrics.languages": "Languages",
|
"metrics.languages": "Хэл",
|
||||||
"metrics.operating-systems": "Үйлдлийн систем",
|
"metrics.operating-systems": "Үйлдлийн систем",
|
||||||
"metrics.page-views": "Хуудас үзсэн",
|
"metrics.page-views": "Хуудас үзсэн",
|
||||||
"metrics.pages": "Хуудас",
|
"metrics.pages": "Хуудас",
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,8 @@
|
||||||
"label.add-website": "Adicionar site",
|
"label.add-website": "Adicionar site",
|
||||||
"label.administrator": "Administrador",
|
"label.administrator": "Administrador",
|
||||||
"label.all": "Todos",
|
"label.all": "Todos",
|
||||||
"label.all-events": "All events",
|
"label.all-events": "Todos os eventos",
|
||||||
"label.all-time": "All time",
|
"label.all-time": "Todo o período",
|
||||||
"label.all-websites": "Todos os sites",
|
"label.all-websites": "Todos os sites",
|
||||||
"label.back": "Voltar",
|
"label.back": "Voltar",
|
||||||
"label.cancel": "Cancelar",
|
"label.cancel": "Cancelar",
|
||||||
|
|
@ -28,7 +28,7 @@
|
||||||
"label.enable-share-url": "Ativar link de compartilhamento",
|
"label.enable-share-url": "Ativar link de compartilhamento",
|
||||||
"label.invalid": "Inválido",
|
"label.invalid": "Inválido",
|
||||||
"label.invalid-domain": "Domínio inválido",
|
"label.invalid-domain": "Domínio inválido",
|
||||||
"label.language": "Language",
|
"label.language": "Idioma",
|
||||||
"label.last-days": "Últimos {x} dias",
|
"label.last-days": "Últimos {x} dias",
|
||||||
"label.last-hours": "Últimas {x} horas",
|
"label.last-hours": "Últimas {x} horas",
|
||||||
"label.logged-in-as": "Sessão iniciada como {username}",
|
"label.logged-in-as": "Sessão iniciada como {username}",
|
||||||
|
|
@ -37,7 +37,7 @@
|
||||||
"label.more": "Mais",
|
"label.more": "Mais",
|
||||||
"label.name": "Nome",
|
"label.name": "Nome",
|
||||||
"label.new-password": "Nova senha",
|
"label.new-password": "Nova senha",
|
||||||
"label.owner": "Owner",
|
"label.owner": "Proprietário",
|
||||||
"label.password": "Senha",
|
"label.password": "Senha",
|
||||||
"label.passwords-dont-match": "As senhas não correspondem",
|
"label.passwords-dont-match": "As senhas não correspondem",
|
||||||
"label.profile": "Perfil",
|
"label.profile": "Perfil",
|
||||||
|
|
@ -46,12 +46,12 @@
|
||||||
"label.refresh": "Atualizar",
|
"label.refresh": "Atualizar",
|
||||||
"label.required": "Obrigatório",
|
"label.required": "Obrigatório",
|
||||||
"label.reset": "Redefinir",
|
"label.reset": "Redefinir",
|
||||||
"label.reset-website": "Reset statistics",
|
"label.reset-website": "Redefinir estatísticas",
|
||||||
"label.save": "Salvar",
|
"label.save": "Salvar",
|
||||||
"label.settings": "Configurações",
|
"label.settings": "Configurações",
|
||||||
"label.share-url": "Link de compartilhamento",
|
"label.share-url": "Link de compartilhamento",
|
||||||
"label.single-day": "Dia específico",
|
"label.single-day": "Dia específico",
|
||||||
"label.theme": "Theme",
|
"label.theme": "Tema",
|
||||||
"label.this-month": "Este mês",
|
"label.this-month": "Este mês",
|
||||||
"label.this-week": "Esta semana",
|
"label.this-week": "Esta semana",
|
||||||
"label.this-year": "Este ano",
|
"label.this-year": "Este ano",
|
||||||
|
|
@ -64,7 +64,7 @@
|
||||||
"label.websites": "Sites",
|
"label.websites": "Sites",
|
||||||
"message.active-users": "{x} {x, plural, one {visitante} other {visitantes}} neste momento",
|
"message.active-users": "{x} {x, plural, one {visitante} other {visitantes}} neste momento",
|
||||||
"message.confirm-delete": "Deseja realmente remover {target}?",
|
"message.confirm-delete": "Deseja realmente remover {target}?",
|
||||||
"message.confirm-reset": "Are your sure you want to reset {target}'s statistics?",
|
"message.confirm-reset": "Você tem certeza que deseja redefinir as estatísticas de {target}?",
|
||||||
"message.copied": "Copiado!",
|
"message.copied": "Copiado!",
|
||||||
"message.delete-warning": "Todos os dados associados também serão eliminados.",
|
"message.delete-warning": "Todos os dados associados também serão eliminados.",
|
||||||
"message.failure": "Ocorreu um erro.",
|
"message.failure": "Ocorreu um erro.",
|
||||||
|
|
@ -78,10 +78,10 @@
|
||||||
"message.no-websites-configured": "Nenhum site foi configurado ainda.",
|
"message.no-websites-configured": "Nenhum site foi configurado ainda.",
|
||||||
"message.page-not-found": "Página não encontrada.",
|
"message.page-not-found": "Página não encontrada.",
|
||||||
"message.powered-by": "Distribuído por {name}",
|
"message.powered-by": "Distribuído por {name}",
|
||||||
"message.reset-warning": "All statistics for this website will be deleted, but your tracking code will remain intact.",
|
"message.reset-warning": "Todas as estatísticas deste site serão removidas, mas seu código de rastreamento permanecerá intacto.",
|
||||||
"message.save-success": "Salvo com sucesso.",
|
"message.save-success": "Salvo com sucesso.",
|
||||||
"message.share-url": "Este é o link público de compartilhamento para {target}.",
|
"message.share-url": "Este é o link público de compartilhamento para {target}.",
|
||||||
"message.toggle-charts": "Toggle charts",
|
"message.toggle-charts": "Mostrar/Esconder gráficos",
|
||||||
"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": "Escreva {reset} abaixo para continuar.",
|
"message.type-reset": "Escreva {reset} abaixo para continuar.",
|
||||||
|
|
@ -99,7 +99,7 @@
|
||||||
"metrics.filter.combined": "Combinado",
|
"metrics.filter.combined": "Combinado",
|
||||||
"metrics.filter.domain-only": "Apenas domínio",
|
"metrics.filter.domain-only": "Apenas domínio",
|
||||||
"metrics.filter.raw": "Dados brutos",
|
"metrics.filter.raw": "Dados brutos",
|
||||||
"metrics.languages": "Languages",
|
"metrics.languages": "Idiomas",
|
||||||
"metrics.operating-systems": "Sistemas operacionais",
|
"metrics.operating-systems": "Sistemas operacionais",
|
||||||
"metrics.page-views": "Visualizações de página",
|
"metrics.page-views": "Visualizações de página",
|
||||||
"metrics.pages": "Páginas",
|
"metrics.pages": "Páginas",
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,7 @@
|
||||||
"label.enable-share-url": "Разрешить делиться ссылкой",
|
"label.enable-share-url": "Разрешить делиться ссылкой",
|
||||||
"label.invalid": "Некорректный",
|
"label.invalid": "Некорректный",
|
||||||
"label.invalid-domain": "Некорректный домен",
|
"label.invalid-domain": "Некорректный домен",
|
||||||
"label.language": "Language",
|
"label.language": "Язык",
|
||||||
"label.last-days": "Последние {x} дней",
|
"label.last-days": "Последние {x} дней",
|
||||||
"label.last-hours": "Последние {x} часа",
|
"label.last-hours": "Последние {x} часа",
|
||||||
"label.logged-in-as": "Вы вошли как {username}",
|
"label.logged-in-as": "Вы вошли как {username}",
|
||||||
|
|
@ -51,7 +51,7 @@
|
||||||
"label.settings": "Настройки",
|
"label.settings": "Настройки",
|
||||||
"label.share-url": "Поделиться ссылкой",
|
"label.share-url": "Поделиться ссылкой",
|
||||||
"label.single-day": "Один день",
|
"label.single-day": "Один день",
|
||||||
"label.theme": "Theme",
|
"label.theme": "Тема",
|
||||||
"label.this-month": "Этот месяц",
|
"label.this-month": "Этот месяц",
|
||||||
"label.this-week": "Эта неделя",
|
"label.this-week": "Эта неделя",
|
||||||
"label.this-year": "Этот год",
|
"label.this-year": "Этот год",
|
||||||
|
|
|
||||||
|
|
@ -2,11 +2,11 @@
|
||||||
"label.accounts": "Tài khoản",
|
"label.accounts": "Tài khoản",
|
||||||
"label.add-account": "Thêm tài khoản",
|
"label.add-account": "Thêm tài khoản",
|
||||||
"label.add-website": "Thêm website",
|
"label.add-website": "Thêm website",
|
||||||
"label.administrator": "Quản Trị",
|
"label.administrator": "Quản trị",
|
||||||
"label.all": "Tất cả",
|
"label.all": "Tất cả",
|
||||||
"label.all-events": "Tất cả events",
|
"label.all-events": "Tất cả sự kiện",
|
||||||
"label.all-time": "All time",
|
"label.all-time": "Toàn thời gian",
|
||||||
"label.all-websites": "Tất cả websites",
|
"label.all-websites": "Tất cả website",
|
||||||
"label.back": "Quay về",
|
"label.back": "Quay về",
|
||||||
"label.cancel": "Huỷ bỏ",
|
"label.cancel": "Huỷ bỏ",
|
||||||
"label.change-password": "Đổi mật khẩu",
|
"label.change-password": "Đổi mật khẩu",
|
||||||
|
|
@ -16,7 +16,7 @@
|
||||||
"label.custom-range": "Phạm vi ngày tuỳ chọn",
|
"label.custom-range": "Phạm vi ngày tuỳ chọn",
|
||||||
"label.dashboard": "Bảng điều khiển",
|
"label.dashboard": "Bảng điều khiển",
|
||||||
"label.date-range": "Phạm vi ngày",
|
"label.date-range": "Phạm vi ngày",
|
||||||
"label.default-date-range": "Phạm vi ngày mặc định",
|
"label.default-date-range": "Khoảng thời gian mặc định",
|
||||||
"label.delete": "Xoá",
|
"label.delete": "Xoá",
|
||||||
"label.delete-account": "Xoá tài khoản",
|
"label.delete-account": "Xoá tài khoản",
|
||||||
"label.delete-website": "Xáo website",
|
"label.delete-website": "Xáo website",
|
||||||
|
|
@ -37,7 +37,7 @@
|
||||||
"label.more": "Thêm",
|
"label.more": "Thêm",
|
||||||
"label.name": "Tên",
|
"label.name": "Tên",
|
||||||
"label.new-password": "Mật khẩu mới",
|
"label.new-password": "Mật khẩu mới",
|
||||||
"label.owner": "Owner",
|
"label.owner": "Chủ nhân",
|
||||||
"label.password": "Mật khẩu",
|
"label.password": "Mật khẩu",
|
||||||
"label.passwords-dont-match": "Mật khẩu không đồng nhất",
|
"label.passwords-dont-match": "Mật khẩu không đồng nhất",
|
||||||
"label.profile": "Hồ sơ",
|
"label.profile": "Hồ sơ",
|
||||||
|
|
@ -81,7 +81,7 @@
|
||||||
"message.reset-warning": "Tất cả số liệu thống kê của website này sẽ bị xoá, nhưng mã theo dõi sẽ vẫn giữ nguyên.",
|
"message.reset-warning": "Tất cả số liệu thống kê của website này sẽ bị xoá, nhưng mã theo dõi sẽ vẫn giữ nguyên.",
|
||||||
"message.save-success": "Đã lưu thành công.",
|
"message.save-success": "Đã lưu thành công.",
|
||||||
"message.share-url": "Đây là đường dẫn URL cho {target}.",
|
"message.share-url": "Đây là đường dẫn URL cho {target}.",
|
||||||
"message.toggle-charts": "Toggle charts",
|
"message.toggle-charts": "Bật/tắt biểu đồ",
|
||||||
"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": "Nhập {reset} bên dưới để xác nhận.",
|
"message.type-reset": "Nhập {reset} bên dưới để xác nhận.",
|
||||||
|
|
@ -99,12 +99,12 @@
|
||||||
"metrics.filter.combined": "Kết hợp",
|
"metrics.filter.combined": "Kết hợp",
|
||||||
"metrics.filter.domain-only": "Chỉ tên miền",
|
"metrics.filter.domain-only": "Chỉ tên miền",
|
||||||
"metrics.filter.raw": "Gốc",
|
"metrics.filter.raw": "Gốc",
|
||||||
"metrics.languages": "Languages",
|
"metrics.languages": "Ngôn ngũ",
|
||||||
"metrics.operating-systems": "Hệ điều hành",
|
"metrics.operating-systems": "Hệ điều hành",
|
||||||
"metrics.page-views": "Lượt xem",
|
"metrics.page-views": "Lượt xem",
|
||||||
"metrics.pages": "Trang",
|
"metrics.pages": "Trang",
|
||||||
"metrics.referrers": "Liên kết giới thiệu",
|
"metrics.referrers": "Liên kết giới thiệu",
|
||||||
"metrics.unique-visitors": "Khách truy cập duy nhất",
|
"metrics.unique-visitors": "Khách truy cập một lần",
|
||||||
"metrics.views": "Xem",
|
"metrics.views": "Xem",
|
||||||
"metrics.visitors": "Khách"
|
"metrics.visitors": "Khách"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { parseSecureToken, parseToken } from './crypto';
|
import { parseSecureToken, parseToken } from './crypto';
|
||||||
import { TOKEN_HEADER } from './constants';
|
import { SHARE_TOKEN_HEADER } from './constants';
|
||||||
import { getWebsiteById } from './queries';
|
import { getWebsiteById } from './queries';
|
||||||
|
|
||||||
export async function getAuthToken(req) {
|
export async function getAuthToken(req) {
|
||||||
|
|
@ -30,7 +30,7 @@ export async function isValidToken(token, validation) {
|
||||||
|
|
||||||
export async function allowQuery(req, skipToken) {
|
export async function allowQuery(req, skipToken) {
|
||||||
const { id } = req.query;
|
const { id } = req.query;
|
||||||
const token = req.headers[TOKEN_HEADER];
|
const token = req.headers[SHARE_TOKEN_HEADER];
|
||||||
const websiteId = +id;
|
const websiteId = +id;
|
||||||
|
|
||||||
const website = await getWebsiteById(websiteId);
|
const website = await getWebsiteById(websiteId);
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ export const DATE_RANGE_CONFIG = 'umami.date-range';
|
||||||
export const THEME_CONFIG = 'umami.theme';
|
export const THEME_CONFIG = 'umami.theme';
|
||||||
export const DASHBOARD_CONFIG = 'umami.dashboard';
|
export const DASHBOARD_CONFIG = 'umami.dashboard';
|
||||||
export const VERSION_CHECK = 'umami.version-check';
|
export const VERSION_CHECK = 'umami.version-check';
|
||||||
export const TOKEN_HEADER = 'x-umami-token';
|
export const SHARE_TOKEN_HEADER = 'x-umami-share-token';
|
||||||
export const HOMEPAGE_URL = 'https://umami.is';
|
export const HOMEPAGE_URL = 'https://umami.is';
|
||||||
export const VERSION_URL = 'https://github.com/mikecao/umami/releases';
|
export const VERSION_URL = 'https://github.com/mikecao/umami/releases';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,11 +14,11 @@ export function hash(...args) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function secret() {
|
export function secret() {
|
||||||
return hash(process.env.HASH_SALT);
|
return hash(process.env.HASH_SALT || process.env.DATABASE_URL);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function salt() {
|
export function salt() {
|
||||||
return v5([secret(), ROTATING_SALT].join(''), v5.DNS);
|
return v5(hash(secret(), ROTATING_SALT), v5.DNS);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function uuid(...args) {
|
export function uuid(...args) {
|
||||||
|
|
|
||||||
10
lib/db.js
10
lib/db.js
|
|
@ -11,23 +11,23 @@ const options = {
|
||||||
};
|
};
|
||||||
|
|
||||||
function logQuery(e) {
|
function logQuery(e) {
|
||||||
if (process.env.LOG_QUERY) {
|
console.log(chalk.yellow(e.params), '->', e.query, chalk.greenBright(`${e.duration}ms`));
|
||||||
console.log(chalk.yellow(e.params), '->', e.query, chalk.greenBright(`${e.duration}ms`));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let prisma;
|
let prisma;
|
||||||
|
|
||||||
if (process.env.NODE_ENV === 'production') {
|
if (process.env.NODE_ENV === 'production') {
|
||||||
prisma = new PrismaClient(options);
|
prisma = new PrismaClient(options);
|
||||||
prisma.$on('query', logQuery);
|
|
||||||
} else {
|
} else {
|
||||||
if (!global.prisma) {
|
if (!global.prisma) {
|
||||||
global.prisma = new PrismaClient(options);
|
global.prisma = new PrismaClient(options);
|
||||||
global.prisma.$on('query', logQuery);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
prisma = global.prisma;
|
prisma = global.prisma;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (process.env.LOG_QUERY) {
|
||||||
|
prisma.$on('query', logQuery);
|
||||||
|
}
|
||||||
|
|
||||||
export default prisma;
|
export default prisma;
|
||||||
|
|
|
||||||
|
|
@ -53,6 +53,7 @@ export const languages = {
|
||||||
'fa-IR': { label: 'فارسی', dateLocale: faIR, dir: 'rtl' },
|
'fa-IR': { label: 'فارسی', dateLocale: faIR, dir: 'rtl' },
|
||||||
'fo-FO': { label: 'Føroyskt' },
|
'fo-FO': { label: 'Føroyskt' },
|
||||||
'fr-FR': { label: 'Français', dateLocale: fr },
|
'fr-FR': { label: 'Français', dateLocale: fr },
|
||||||
|
'ga-ES': { label: 'Galacian (Spain)', dateLocale: es },
|
||||||
'el-GR': { label: 'Ελληνικά', dateLocale: el },
|
'el-GR': { label: 'Ελληνικά', dateLocale: el },
|
||||||
'he-IL': { label: 'עברית', dateLocale: he },
|
'he-IL': { label: 'עברית', dateLocale: he },
|
||||||
'hi-IN': { label: 'हिन्दी', dateLocale: hi },
|
'hi-IN': { label: 'हिन्दी', dateLocale: hi },
|
||||||
|
|
|
||||||
|
|
@ -64,7 +64,7 @@ export function getFilterQuery(table, filters = {}, params = []) {
|
||||||
|
|
||||||
switch (key) {
|
switch (key) {
|
||||||
case 'url':
|
case 'url':
|
||||||
if (table === 'session' || table === 'pageview') {
|
if (table === 'pageview' || table === 'event') {
|
||||||
arr.push(`and ${table}.${key}=$${params.length + 1}`);
|
arr.push(`and ${table}.${key}=$${params.length + 1}`);
|
||||||
params.push(decodeURIComponent(value));
|
params.push(decodeURIComponent(value));
|
||||||
}
|
}
|
||||||
|
|
@ -110,11 +110,11 @@ export function getFilterQuery(table, filters = {}, params = []) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function parseFilters(table, filters = {}, params = []) {
|
export function parseFilters(table, filters = {}, params = []) {
|
||||||
const { domain, url, referrer, os, browser, device, country, event_type } = filters;
|
const { domain, url, event_url, referrer, os, browser, device, country, event_type } = filters;
|
||||||
|
|
||||||
const pageviewFilters = { domain, url, referrer };
|
const pageviewFilters = { domain, url, referrer };
|
||||||
const sessionFilters = { os, browser, device, country };
|
const sessionFilters = { os, browser, device, country };
|
||||||
const eventFilters = { event_type };
|
const eventFilters = { url: event_url, event_type };
|
||||||
|
|
||||||
return {
|
return {
|
||||||
pageviewFilters,
|
pageviewFilters,
|
||||||
|
|
@ -315,6 +315,13 @@ export async function getAccounts() {
|
||||||
username: 'asc',
|
username: 'asc',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
select: {
|
||||||
|
user_id: true,
|
||||||
|
username: true,
|
||||||
|
is_admin: true,
|
||||||
|
created_at: true,
|
||||||
|
updated_at: true,
|
||||||
|
},
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -502,8 +509,11 @@ export function getSessionMetrics(website_id, start_at, end_at, field, filters =
|
||||||
|
|
||||||
export function getPageviewMetrics(website_id, start_at, end_at, field, table, filters = {}) {
|
export function getPageviewMetrics(website_id, start_at, end_at, field, table, filters = {}) {
|
||||||
const params = [website_id, start_at, end_at];
|
const params = [website_id, start_at, end_at];
|
||||||
console.log({ table, filters });
|
const { pageviewQuery, sessionQuery, eventQuery, joinSession } = parseFilters(
|
||||||
const { pageviewQuery, sessionQuery, joinSession } = parseFilters(table, filters, params);
|
table,
|
||||||
|
filters,
|
||||||
|
params,
|
||||||
|
);
|
||||||
|
|
||||||
return rawQuery(
|
return rawQuery(
|
||||||
`
|
`
|
||||||
|
|
@ -514,6 +524,7 @@ export function getPageviewMetrics(website_id, start_at, end_at, field, table, f
|
||||||
and ${table}.created_at between $2 and $3
|
and ${table}.created_at between $2 and $3
|
||||||
${pageviewQuery}
|
${pageviewQuery}
|
||||||
${joinSession && sessionQuery}
|
${joinSession && sessionQuery}
|
||||||
|
${eventQuery}
|
||||||
group by 1
|
group by 1
|
||||||
order by 2 desc
|
order by 2 desc
|
||||||
`,
|
`,
|
||||||
|
|
|
||||||
|
|
@ -3,9 +3,14 @@ const pkg = require('./package.json');
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
env: {
|
env: {
|
||||||
VERSION: pkg.version,
|
currentVersion: pkg.version,
|
||||||
|
loginDisabled: process.env.DISABLE_LOGIN,
|
||||||
|
updatesDisabled: process.env.DISABLE_UPDATES,
|
||||||
},
|
},
|
||||||
basePath: process.env.BASE_PATH,
|
basePath: process.env.BASE_PATH,
|
||||||
|
experimental: {
|
||||||
|
outputStandalone: true,
|
||||||
|
},
|
||||||
eslint: {
|
eslint: {
|
||||||
ignoreDuringBuilds: true,
|
ignoreDuringBuilds: true,
|
||||||
},
|
},
|
||||||
|
|
|
||||||
32
package.json
32
package.json
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "umami",
|
"name": "umami",
|
||||||
"version": "1.30.0",
|
"version": "1.33.0",
|
||||||
"description": "A simple, fast, privacy-focused alternative to Google Analytics.",
|
"description": "A simple, fast, privacy-focused alternative to Google Analytics.",
|
||||||
"author": "Mike Cao <mike@mikecao.com>",
|
"author": "Mike Cao <mike@mikecao.com>",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|
@ -12,20 +12,21 @@
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev",
|
"dev": "next dev",
|
||||||
"build": "npm-run-all build-tracker build-geo build-db build-app",
|
"build": "npm-run-all build-tracker build-geo build-db build-app",
|
||||||
"start": "next start",
|
"start": "npm-run-all check-db start-next",
|
||||||
|
"start-docker": "npm-run-all check-db start-server",
|
||||||
"start-env": "node -r dotenv/config scripts/start-env.js",
|
"start-env": "node -r dotenv/config scripts/start-env.js",
|
||||||
|
"start-server": "node server.js",
|
||||||
|
"start-next": "next start",
|
||||||
"build-app": "next build",
|
"build-app": "next build",
|
||||||
"build-tracker": "rollup -c rollup.tracker.config.js",
|
"build-tracker": "rollup -c rollup.tracker.config.js",
|
||||||
"build-db": "npm-run-all copy-db-schema build-db-client",
|
"build-db": "npm-run-all copy-db-files build-db-client",
|
||||||
"build-lang": "npm-run-all format-lang compile-lang",
|
"build-lang": "npm-run-all format-lang compile-lang",
|
||||||
"build-geo": "node scripts/build-geo.js",
|
"build-geo": "node scripts/build-geo.js",
|
||||||
"build-db-schema": "dotenv prisma introspect",
|
"build-db-schema": "prisma db pull",
|
||||||
"build-db-client": "dotenv prisma generate",
|
"build-db-client": "prisma generate",
|
||||||
"build-mysql-schema": "dotenv prisma db pull -- --schema=./prisma/schema.mysql.prisma",
|
"update-db": "prisma migrate deploy",
|
||||||
"build-mysql-client": "dotenv prisma generate -- --schema=./prisma/schema.mysql.prisma",
|
"check-db": "node scripts/check-db.js",
|
||||||
"build-postgresql-schema": "dotenv prisma db pull -- --schema=./prisma/schema.postgresql.prisma",
|
"copy-db-files": "node scripts/copy-db-files.js",
|
||||||
"build-postgresql-client": "dotenv prisma generate -- --schema=./prisma/schema.postgresql.prisma",
|
|
||||||
"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",
|
||||||
"merge-lang": "node scripts/merge-lang.js",
|
"merge-lang": "node scripts/merge-lang.js",
|
||||||
|
|
@ -36,7 +37,7 @@
|
||||||
"download-language-names": "node scripts/download-language-names.js",
|
"download-language-names": "node scripts/download-language-names.js",
|
||||||
"change-password": "node scripts/change-password.js",
|
"change-password": "node scripts/change-password.js",
|
||||||
"lint": "next lint --quiet",
|
"lint": "next lint --quiet",
|
||||||
"prepare": "husky install",
|
"prepare": "node -e \"if (process.env.NODE_ENV !== 'production'){process.exit(1)} \" || husky install",
|
||||||
"postbuild": "node scripts/postbuild.js"
|
"postbuild": "node scripts/postbuild.js"
|
||||||
},
|
},
|
||||||
"lint-staged": {
|
"lint-staged": {
|
||||||
|
|
@ -54,13 +55,14 @@
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fontsource/inter": "4.5.7",
|
"@fontsource/inter": "4.5.7",
|
||||||
"@prisma/client": "3.12.0",
|
"@prisma/client": "3.15.2",
|
||||||
"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",
|
||||||
"classnames": "^2.3.1",
|
"classnames": "^2.3.1",
|
||||||
"colord": "^2.9.2",
|
"colord": "^2.9.2",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
|
"cross-spawn": "^7.0.3",
|
||||||
"date-fns": "^2.23.0",
|
"date-fns": "^2.23.0",
|
||||||
"date-fns-tz": "^1.1.4",
|
"date-fns-tz": "^1.1.4",
|
||||||
"del": "^6.0.0",
|
"del": "^6.0.0",
|
||||||
|
|
@ -78,7 +80,7 @@
|
||||||
"jose": "2.0.5",
|
"jose": "2.0.5",
|
||||||
"maxmind": "^4.3.6",
|
"maxmind": "^4.3.6",
|
||||||
"moment-timezone": "^0.5.33",
|
"moment-timezone": "^0.5.33",
|
||||||
"next": "12.1.4",
|
"next": "12.1.0",
|
||||||
"node-fetch": "^3.2.3",
|
"node-fetch": "^3.2.3",
|
||||||
"npm-run-all": "^4.1.5",
|
"npm-run-all": "^4.1.5",
|
||||||
"prop-types": "^15.7.2",
|
"prop-types": "^15.7.2",
|
||||||
|
|
@ -112,10 +114,10 @@
|
||||||
"postcss": "^8.4.12",
|
"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.3",
|
||||||
"postcss-rtlcss": "^3.6.1",
|
"postcss-rtlcss": "^3.6.1",
|
||||||
"prettier": "^2.6.2",
|
"prettier": "^2.6.2",
|
||||||
"prisma": "3.12.0",
|
"prisma": "3.15.2",
|
||||||
"prompts": "2.4.2",
|
"prompts": "2.4.2",
|
||||||
"rollup": "^2.70.1",
|
"rollup": "^2.70.1",
|
||||||
"rollup-plugin-terser": "^7.0.2",
|
"rollup-plugin-terser": "^7.0.2",
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,6 @@ const Intl = ({ children }) => {
|
||||||
export default function App({ Component, pageProps }) {
|
export default function App({ Component, pageProps }) {
|
||||||
const { basePath } = useRouter();
|
const { basePath } = useRouter();
|
||||||
const { dir } = useLocale();
|
const { dir } = useLocale();
|
||||||
const version = process.env.VERSION;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Intl>
|
<Intl>
|
||||||
|
|
@ -35,12 +34,6 @@ export default function App({ Component, pageProps }) {
|
||||||
<link rel="icon" type="image/png" sizes="16x16" href={`${basePath}/favicon-16x16.png`} />
|
<link rel="icon" type="image/png" sizes="16x16" href={`${basePath}/favicon-16x16.png`} />
|
||||||
<link rel="manifest" href={`${basePath}/site.webmanifest`} />
|
<link rel="manifest" href={`${basePath}/site.webmanifest`} />
|
||||||
<link rel="mask-icon" href={`${basePath}/safari-pinned-tab.svg`} color="#5bbad5" />
|
<link rel="mask-icon" href={`${basePath}/safari-pinned-tab.svg`} color="#5bbad5" />
|
||||||
<link
|
|
||||||
rel="preload"
|
|
||||||
href={`https://i.umami.is/icon.png?v=${version}`}
|
|
||||||
as="image"
|
|
||||||
type="image/png"
|
|
||||||
/>
|
|
||||||
<meta name="msapplication-TileColor" content="#da532c" />
|
<meta name="msapplication-TileColor" content="#da532c" />
|
||||||
<meta name="theme-color" content="#fafafa" media="(prefers-color-scheme: light)" />
|
<meta name="theme-color" content="#fafafa" media="(prefers-color-scheme: light)" />
|
||||||
<meta name="theme-color" content="#2f2f2f" media="(prefers-color-scheme: dark)" />
|
<meta name="theme-color" content="#2f2f2f" media="(prefers-color-scheme: dark)" />
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ function customScriptName(req) {
|
||||||
if (scriptName) {
|
if (scriptName) {
|
||||||
const url = req.nextUrl.clone();
|
const url = req.nextUrl.clone();
|
||||||
const { pathname } = url;
|
const { pathname } = url;
|
||||||
const names = scriptName.split(',').map(name => (name + '.js').trim());
|
const names = scriptName.split(',').map(name => name.trim() + '.js');
|
||||||
|
|
||||||
if (names.find(name => pathname.endsWith(name))) {
|
if (names.find(name => pathname.endsWith(name))) {
|
||||||
url.pathname = '/umami.js';
|
url.pathname = '/umami.js';
|
||||||
|
|
@ -15,12 +15,6 @@ function customScriptName(req) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function disableLogin(req) {
|
|
||||||
if (process.env.DISABLE_LOGIN && req.nextUrl.pathname.endsWith('/login')) {
|
|
||||||
return new Response('403 Forbidden', { status: 403 });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function forceSSL(req, res) {
|
function forceSSL(req, res) {
|
||||||
if (process.env.FORCE_SSL && req.nextUrl.protocol === 'http:') {
|
if (process.env.FORCE_SSL && req.nextUrl.protocol === 'http:') {
|
||||||
res.headers.set('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');
|
res.headers.set('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');
|
||||||
|
|
@ -30,7 +24,7 @@ function forceSSL(req, res) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function middleware(req) {
|
export function middleware(req) {
|
||||||
const fns = [customScriptName, disableLogin];
|
const fns = [customScriptName];
|
||||||
|
|
||||||
for (const fn of fns) {
|
for (const fn of fns) {
|
||||||
const res = fn(req);
|
const res = fn(req);
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,10 @@
|
||||||
|
const { Resolver } = require('dns').promises;
|
||||||
import isbot from 'isbot';
|
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 { getJsonBody, getIpAddress } from 'lib/request';
|
import { getJsonBody, getIpAddress } from 'lib/request';
|
||||||
import { ok, send, badRequest } from 'lib/response';
|
import { ok, send, badRequest, forbidden } from 'lib/response';
|
||||||
import { createToken } from 'lib/crypto';
|
import { createToken } from 'lib/crypto';
|
||||||
import { removeTrailingSlash } from 'lib/url';
|
import { removeTrailingSlash } from 'lib/url';
|
||||||
|
|
||||||
|
|
@ -15,16 +16,35 @@ export default async (req, res) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const ignoreIps = process.env.IGNORE_IP;
|
const ignoreIps = process.env.IGNORE_IP;
|
||||||
if (ignoreIps) {
|
const ignoreHostnames = process.env.IGNORE_HOSTNAME;
|
||||||
const ips = ignoreIps.split(',').map(n => n.trim());
|
|
||||||
const ip = getIpAddress(req);
|
if (ignoreIps || ignoreHostnames) {
|
||||||
const blocked = ips.find(i => {
|
const ips = [];
|
||||||
if (i === ip) return true;
|
|
||||||
|
if (ignoreIps) {
|
||||||
|
ips.push(...ignoreIps.split(',').map(n => n.trim()));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ignoreHostnames) {
|
||||||
|
const resolver = new Resolver();
|
||||||
|
const promises = ignoreHostnames
|
||||||
|
.split(',')
|
||||||
|
.map(n => resolver.resolve4(n.trim()).catch(() => {}));
|
||||||
|
|
||||||
|
await Promise.all(promises).then(resolvedIps => {
|
||||||
|
ips.push(...resolvedIps.filter(n => n).flatMap(n => n));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const clientIp = getIpAddress(req);
|
||||||
|
|
||||||
|
const blocked = ips.find(ip => {
|
||||||
|
if (ip === clientIp) return true;
|
||||||
|
|
||||||
// CIDR notation
|
// CIDR notation
|
||||||
if (i.indexOf('/') > 0) {
|
if (ip.indexOf('/') > 0) {
|
||||||
const addr = ipaddr.parse(ip);
|
const addr = ipaddr.parse(clientIp);
|
||||||
const range = ipaddr.parseCIDR(i);
|
const range = ipaddr.parseCIDR(ip);
|
||||||
|
|
||||||
if (addr.kind() === range[0].kind() && addr.match(range)) return true;
|
if (addr.kind() === range[0].kind() && addr.match(range)) return true;
|
||||||
}
|
}
|
||||||
|
|
@ -33,7 +53,7 @@ export default async (req, res) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
if (blocked) {
|
if (blocked) {
|
||||||
return ok(res);
|
return forbidden(res);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ import { useAuth } from 'lib/middleware';
|
||||||
import { ok, methodNotAllowed, badRequest } from 'lib/response';
|
import { ok, methodNotAllowed, badRequest } from 'lib/response';
|
||||||
import { getRealtimeData } from 'lib/queries';
|
import { getRealtimeData } from 'lib/queries';
|
||||||
import { parseToken } from 'lib/crypto';
|
import { parseToken } from 'lib/crypto';
|
||||||
import { TOKEN_HEADER } from 'lib/constants';
|
import { SHARE_TOKEN_HEADER } from 'lib/constants';
|
||||||
|
|
||||||
export default async (req, res) => {
|
export default async (req, res) => {
|
||||||
await useAuth(req, res);
|
await useAuth(req, res);
|
||||||
|
|
@ -10,7 +10,7 @@ export default async (req, res) => {
|
||||||
if (req.method === 'GET') {
|
if (req.method === 'GET') {
|
||||||
const { start_at } = req.query;
|
const { start_at } = req.query;
|
||||||
|
|
||||||
const token = req.headers[TOKEN_HEADER];
|
const token = req.headers[SHARE_TOKEN_HEADER];
|
||||||
|
|
||||||
if (!token) {
|
if (!token) {
|
||||||
return badRequest(res);
|
return badRequest(res);
|
||||||
|
|
|
||||||
|
|
@ -83,12 +83,13 @@ export default async (req, res) => {
|
||||||
|
|
||||||
const data = await getPageviewMetrics(websiteId, startDate, endDate, column, table, {
|
const data = await getPageviewMetrics(websiteId, startDate, endDate, column, table, {
|
||||||
domain,
|
domain,
|
||||||
url: type !== 'url' ? url : undefined,
|
url: type !== 'url' && table !== 'event' ? url : undefined,
|
||||||
referrer: type !== 'referrer' ? referrer : undefined,
|
referrer: type !== 'referrer' ? referrer : undefined,
|
||||||
os: type !== 'os' ? os : undefined,
|
os: type !== 'os' ? os : undefined,
|
||||||
browser: type !== 'browser' ? browser : undefined,
|
browser: type !== 'browser' ? browser : undefined,
|
||||||
device: type !== 'device' ? device : undefined,
|
device: type !== 'device' ? device : undefined,
|
||||||
country: type !== 'country' ? country : undefined,
|
country: type !== 'country' ? country : undefined,
|
||||||
|
event_url: type !== 'url' && table === 'event' ? url : undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
return ok(res, data);
|
return ok(res, data);
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ import Layout from 'components/layout/Layout';
|
||||||
import LoginForm from 'components/forms/LoginForm';
|
import LoginForm from 'components/forms/LoginForm';
|
||||||
|
|
||||||
export default function LoginPage() {
|
export default function LoginPage() {
|
||||||
if (process.env.DISABLE_LOGIN) {
|
if (process.env.loginDisabled) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
../schema.mysql.prisma
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
../schema.postgresql.prisma
|
|
||||||
|
|
@ -1,29 +0,0 @@
|
||||||
const bcrypt = require('bcryptjs');
|
|
||||||
const { PrismaClient } = require('@prisma/client');
|
|
||||||
const prisma = new PrismaClient();
|
|
||||||
const SALT_ROUNDS = 10;
|
|
||||||
|
|
||||||
const hashPassword = password => {
|
|
||||||
return bcrypt.hashSync(password, SALT_ROUNDS);
|
|
||||||
};
|
|
||||||
|
|
||||||
async function main() {
|
|
||||||
await prisma.account.upsert({
|
|
||||||
where: { username: 'admin' },
|
|
||||||
update: {},
|
|
||||||
create: {
|
|
||||||
username: 'admin',
|
|
||||||
password: hashPassword(process.env.ADMIN_PASSWORD || 'umami'),
|
|
||||||
is_admin: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
main()
|
|
||||||
.catch(e => {
|
|
||||||
console.error(e);
|
|
||||||
process.exit(1);
|
|
||||||
})
|
|
||||||
.finally(async () => {
|
|
||||||
await prisma.$disconnect();
|
|
||||||
});
|
|
||||||
1
public/intl/country/ga-ES.json
Normal file
1
public/intl/country/ga-ES.json
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
404: Not Found
|
||||||
1
public/intl/language/ga-ES.json
Normal file
1
public/intl/language/ga-ES.json
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
404: Not Found
|
||||||
|
|
@ -176,7 +176,7 @@
|
||||||
"label.language": [
|
"label.language": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Language"
|
"value": "Sprache"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.last-days": [
|
"label.last-days": [
|
||||||
|
|
@ -334,7 +334,7 @@
|
||||||
"label.theme": [
|
"label.theme": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Theme"
|
"value": "Thema"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.this-month": [
|
"label.this-month": [
|
||||||
|
|
@ -704,7 +704,7 @@
|
||||||
"metrics.device.mobile": [
|
"metrics.device.mobile": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Mobiltelefon"
|
"value": "Handy"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"metrics.device.tablet": [
|
"metrics.device.tablet": [
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,7 @@
|
||||||
"label.all-time": [
|
"label.all-time": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "All time"
|
"value": "همه زمان"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.all-websites": [
|
"label.all-websites": [
|
||||||
|
|
@ -176,7 +176,7 @@
|
||||||
"label.language": [
|
"label.language": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Language"
|
"value": "زبان"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.last-days": [
|
"label.last-days": [
|
||||||
|
|
@ -190,7 +190,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": " روز"
|
"value": " روز گذشته"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.last-hours": [
|
"label.last-hours": [
|
||||||
|
|
@ -204,7 +204,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": " ساعت"
|
"value": " ساعت گذشته"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.logged-in-as": [
|
"label.logged-in-as": [
|
||||||
|
|
@ -250,7 +250,7 @@
|
||||||
"label.owner": [
|
"label.owner": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Owner"
|
"value": "ایجاد شده توسط"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.password": [
|
"label.password": [
|
||||||
|
|
@ -304,7 +304,7 @@
|
||||||
"label.reset-website": [
|
"label.reset-website": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Reset statistics"
|
"value": "بازنشانی آمار"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.save": [
|
"label.save": [
|
||||||
|
|
@ -334,7 +334,7 @@
|
||||||
"label.theme": [
|
"label.theme": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Theme"
|
"value": "تم"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.this-month": [
|
"label.this-month": [
|
||||||
|
|
@ -448,7 +448,7 @@
|
||||||
"message.confirm-reset": [
|
"message.confirm-reset": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Are your sure you want to reset "
|
"value": "آیا از بازنشانی آمار "
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": 1,
|
"type": 1,
|
||||||
|
|
@ -456,7 +456,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "'s statistics?"
|
"value": " مطمئن هستید?"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"message.copied": [
|
"message.copied": [
|
||||||
|
|
@ -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": "تمامی آمارهای این وبسایت حذف خواهد شد اما tracking code بدون تغییر باقی میماند."
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"message.save-success": [
|
"message.save-success": [
|
||||||
|
|
@ -730,7 +730,7 @@
|
||||||
"metrics.languages": [
|
"metrics.languages": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Languages"
|
"value": "زبانها"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"metrics.operating-systems": [
|
"metrics.operating-systems": [
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,7 @@
|
||||||
"label.all-time": [
|
"label.all-time": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Toutes périodes"
|
"value": "Toutes les données"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.all-websites": [
|
"label.all-websites": [
|
||||||
|
|
@ -86,7 +86,7 @@
|
||||||
"label.custom-range": [
|
"label.custom-range": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Intervalle personnalisé"
|
"value": "Période personnalisée"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.dashboard": [
|
"label.dashboard": [
|
||||||
|
|
@ -98,13 +98,13 @@
|
||||||
"label.date-range": [
|
"label.date-range": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Intervalle"
|
"value": "Période"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.default-date-range": [
|
"label.default-date-range": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Intervalle par défaut"
|
"value": "Période par défaut"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.delete": [
|
"label.delete": [
|
||||||
|
|
@ -158,7 +158,7 @@
|
||||||
"label.enable-share-url": [
|
"label.enable-share-url": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Activer le partage d'URL"
|
"value": "Activer l'URL de partage"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.invalid": [
|
"label.invalid": [
|
||||||
|
|
@ -476,13 +476,13 @@
|
||||||
"message.get-share-url": [
|
"message.get-share-url": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Obtenez l'URL de partage"
|
"value": "Obtenir l'URL de partage"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"message.get-tracking-code": [
|
"message.get-tracking-code": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Obtenez le code de suivi"
|
"value": "Obtenir le code de suivi"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"message.go-to-settings": [
|
"message.go-to-settings": [
|
||||||
|
|
@ -494,7 +494,7 @@
|
||||||
"message.incorrect-username-password": [
|
"message.incorrect-username-password": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "nom d'utilisateurs/mot de passe incorrect."
|
"value": "Nom d'utilisateur/Mot de passe incorrect."
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"message.log.visitor": [
|
"message.log.visitor": [
|
||||||
|
|
@ -602,7 +602,7 @@
|
||||||
"message.toggle-charts": [
|
"message.toggle-charts": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Changer les graphiques"
|
"value": "Afficher/Masquer les graphiques"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"message.track-stats": [
|
"message.track-stats": [
|
||||||
|
|
@ -736,7 +736,7 @@
|
||||||
"metrics.filter.raw": [
|
"metrics.filter.raw": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Brute"
|
"value": "Brut"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"metrics.languages": [
|
"metrics.languages": [
|
||||||
|
|
@ -766,7 +766,7 @@
|
||||||
"metrics.referrers": [
|
"metrics.referrers": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "URL Référentes"
|
"value": "Sources"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"metrics.unique-visitors": [
|
"metrics.unique-visitors": [
|
||||||
|
|
|
||||||
794
public/intl/messages/ga-ES.json
Normal file
794
public/intl/messages/ga-ES.json
Normal file
|
|
@ -0,0 +1,794 @@
|
||||||
|
{
|
||||||
|
"label.accounts": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Contas"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"label.add-account": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Engadir conta"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"label.add-website": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Engadir sitio web"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"label.administrator": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Administradora"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"label.all": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Todo"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"label.all-events": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Tódolos eventos"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"label.all-time": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Sempre"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"label.all-websites": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Tódolos sitios web"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"label.back": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Atrás"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"label.cancel": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Cancelar"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"label.change-password": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Mudar contrasinal"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"label.confirm-password": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Confirmar contrasinal"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"label.copy-to-clipboard": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Copiar ao portapapeis"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"label.current-password": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Contrasinal actual"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"label.custom-range": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Rango personalizado"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"label.dashboard": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Taboleiro"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"label.date-range": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Rango temporal"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"label.default-date-range": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Rango temporal por defecto"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"label.delete": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Eliminar"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"label.delete-account": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Eliminar conta"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"label.delete-website": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Eliminar sitio web"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"label.dismiss": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Desbotar"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"label.domain": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Dominio"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"label.edit": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Editar"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"label.edit-account": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Editar conta"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"label.edit-website": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Editar sitio web"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"label.enable-share-url": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Activar URL de compartición"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"label.invalid": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Non válido"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"label.invalid-domain": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Dominio non válido"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"label.language": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Idioma"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"label.last-days": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Últimos "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": 1,
|
||||||
|
"value": "x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": " días"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"label.last-hours": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Últimas "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": 1,
|
||||||
|
"value": "x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": " horas"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"label.logged-in-as": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Sesión de "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": 1,
|
||||||
|
"value": "username"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"label.login": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Acceder"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"label.logout": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Pechar sesión"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"label.more": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Máis"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"label.name": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Nome"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"label.new-password": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Novo contrasinal"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"label.owner": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Dona"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"label.password": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Contrasinal"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"label.passwords-dont-match": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Non concordan os contrasinais"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"label.profile": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Perfil"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"label.realtime": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Agora mesmo"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"label.realtime-logs": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Rexistro neste intre"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"label.refresh": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Actualizar"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"label.required": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Requerido"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"label.reset": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Restablecer"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"label.reset-website": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Restablecer estatísticas"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"label.save": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Gardar"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"label.settings": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Axustes"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"label.share-url": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Compartir URL"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"label.single-day": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Un só día"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"label.theme": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Decorado"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"label.this-month": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Este mes"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"label.this-week": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Esta semana"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"label.this-year": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Este ano"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"label.timezone": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Zona horaria"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"label.today": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Hoxe"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"label.tracking-code": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Código de seguimento"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"label.unknown": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Descoñecido"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"label.username": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Identificador"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"label.view-details": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Ver detalles"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"label.websites": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Sitios web"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"message.active-users": [
|
||||||
|
{
|
||||||
|
"type": 1,
|
||||||
|
"value": "x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": " actual "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"offset": 0,
|
||||||
|
"options": {
|
||||||
|
"one": {
|
||||||
|
"value": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "visitante"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"other": {
|
||||||
|
"value": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "visitantes"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"pluralType": "cardinal",
|
||||||
|
"type": 6,
|
||||||
|
"value": "x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"message.confirm-delete": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Tes a certeza de querer eliminar "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": 1,
|
||||||
|
"value": "target"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "?"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"message.confirm-reset": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Tes a certeza de querer restablecer as estatísticas de "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": 1,
|
||||||
|
"value": "target"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "?"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"message.copied": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Copiado!"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"message.delete-warning": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Tamén serán borrados tódolos datos asociados."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"message.failure": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Houbo un fallo."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"message.get-share-url": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Obter URL de compartición"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"message.get-tracking-code": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Obter código de seguimento"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"message.go-to-settings": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Ir aos axustes"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"message.incorrect-username-password": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Credenciais incorrectas."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"message.log.visitor": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Visitante desde "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": 1,
|
||||||
|
"value": "country"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": " usando "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": 1,
|
||||||
|
"value": "browser"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": " en "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": 1,
|
||||||
|
"value": "os"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": " "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": 1,
|
||||||
|
"value": "device"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"message.new-version-available": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "A nova versión "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": 1,
|
||||||
|
"value": "version"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": " de umami está dispoñible!"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"message.no-data-available": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Sen datos dispoñibles."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"message.no-websites-configured": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Non tes sitios web configurados."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"message.page-not-found": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Páxina non atopada."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"message.powered-by": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Funciona grazas a "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": 1,
|
||||||
|
"value": "name"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"message.reset-warning": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Vanse eliminar tódalas estatísticas deste sitio web, pero o código de seguimento permanecerá sen cambios."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"message.save-success": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Gardouse correctamente."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"message.share-url": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Este é o URL da compartición pública de "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": 1,
|
||||||
|
"value": "target"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"message.toggle-charts": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Activación das gráficas"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"message.track-stats": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Para crear estatísticas de "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": 1,
|
||||||
|
"value": "target"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": ", pon este código na sección "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": 1,
|
||||||
|
"value": "head"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": " do teu sitio web."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"message.type-delete": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Escribe "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": 1,
|
||||||
|
"value": "delete"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": " na caixa inferior para confirmar."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"message.type-reset": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Escribe "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": 1,
|
||||||
|
"value": "reset"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": " na caixa inferior para confirmar."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"metrics.actions": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Accións"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"metrics.average-visit-time": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Tempo medio de visita"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"metrics.bounce-rate": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Proporción de rebote"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"metrics.browsers": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Navegadores"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"metrics.countries": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Países"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"metrics.device.desktop": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Escritorio"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"metrics.device.laptop": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Portátil"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"metrics.device.mobile": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Móbil"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"metrics.device.tablet": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Tableta"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"metrics.devices": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Dispositivos"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"metrics.events": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Eventos"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"metrics.filter.combined": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Combinado"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"metrics.filter.domain-only": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Só dominio"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"metrics.filter.raw": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Raw"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"metrics.languages": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Idiomas"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"metrics.operating-systems": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Sistemas operativos"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"metrics.page-views": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Vistas de páxinas"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"metrics.pages": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Páxinas"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"metrics.referrers": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Orixes"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"metrics.unique-visitors": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Visitas únicas"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"metrics.views": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Visualizacións"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"metrics.visitors": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Visitantes"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -80,7 +80,7 @@
|
||||||
"label.current-password": [
|
"label.current-password": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Password corrente"
|
"value": "Password attuale"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.custom-range": [
|
"label.custom-range": [
|
||||||
|
|
@ -176,7 +176,7 @@
|
||||||
"label.language": [
|
"label.language": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Language"
|
"value": "Lingua"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.last-days": [
|
"label.last-days": [
|
||||||
|
|
@ -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": "Sei sicuro di voler azzerare le statistiche di "
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": 1,
|
"type": 1,
|
||||||
|
|
@ -460,7 +460,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "'s statistics?"
|
"value": "?"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"message.copied": [
|
"message.copied": [
|
||||||
|
|
@ -542,7 +542,7 @@
|
||||||
"message.new-version-available": [
|
"message.new-version-available": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Una nuova versione umami "
|
"value": "Una nuova versione "
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": 1,
|
"type": 1,
|
||||||
|
|
@ -550,7 +550,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": " è disponibile!"
|
"value": " di umami è disponibile!"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"message.no-data-available": [
|
"message.no-data-available": [
|
||||||
|
|
@ -774,7 +774,7 @@
|
||||||
"metrics.referrers": [
|
"metrics.referrers": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Referr"
|
"value": "Referrers"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"metrics.unique-visitors": [
|
"metrics.unique-visitors": [
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,7 @@
|
||||||
"label.all-time": [
|
"label.all-time": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "All time"
|
"value": "Бүх цаг үеийн"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.all-websites": [
|
"label.all-websites": [
|
||||||
|
|
@ -176,7 +176,7 @@
|
||||||
"label.language": [
|
"label.language": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Language"
|
"value": "Хэл"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.last-days": [
|
"label.last-days": [
|
||||||
|
|
@ -250,7 +250,7 @@
|
||||||
"label.owner": [
|
"label.owner": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Owner"
|
"value": "Эзэмшигч"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.password": [
|
"label.password": [
|
||||||
|
|
@ -304,7 +304,7 @@
|
||||||
"label.reset-website": [
|
"label.reset-website": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Reset statistics"
|
"value": "Тоон үзүүлэлтийг дахин эхлүүлэх"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.save": [
|
"label.save": [
|
||||||
|
|
@ -334,7 +334,7 @@
|
||||||
"label.theme": [
|
"label.theme": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Theme"
|
"value": "Загвар"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.this-month": [
|
"label.this-month": [
|
||||||
|
|
@ -588,7 +588,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": "Энэ вебийн бүх тоон үзүүлэлтүүдийг устгах болно. Гэхдээ мөрдөх код хэвэндээ үлдэнэ."
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"message.save-success": [
|
"message.save-success": [
|
||||||
|
|
@ -610,7 +610,7 @@
|
||||||
"message.toggle-charts": [
|
"message.toggle-charts": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Toggle charts"
|
"value": "Графикийг харуулах/нуух"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"message.track-stats": [
|
"message.track-stats": [
|
||||||
|
|
@ -746,7 +746,7 @@
|
||||||
"metrics.languages": [
|
"metrics.languages": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Languages"
|
"value": "Хэл"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"metrics.operating-systems": [
|
"metrics.operating-systems": [
|
||||||
|
|
|
||||||
|
|
@ -32,13 +32,13 @@
|
||||||
"label.all-events": [
|
"label.all-events": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "All events"
|
"value": "Todos os eventos"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.all-time": [
|
"label.all-time": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "All time"
|
"value": "Todo o período"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.all-websites": [
|
"label.all-websites": [
|
||||||
|
|
@ -176,7 +176,7 @@
|
||||||
"label.language": [
|
"label.language": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Language"
|
"value": "Idioma"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.last-days": [
|
"label.last-days": [
|
||||||
|
|
@ -250,7 +250,7 @@
|
||||||
"label.owner": [
|
"label.owner": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Owner"
|
"value": "Proprietário"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.password": [
|
"label.password": [
|
||||||
|
|
@ -304,7 +304,7 @@
|
||||||
"label.reset-website": [
|
"label.reset-website": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Reset statistics"
|
"value": "Redefinir estatísticas"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"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": "Você tem certeza que deseja redefinir as estatísticas de "
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": 1,
|
"type": 1,
|
||||||
|
|
@ -460,7 +460,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "'s statistics?"
|
"value": "?"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"message.copied": [
|
"message.copied": [
|
||||||
|
|
@ -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": "Todas as estatísticas deste site serão removidas, mas seu código de rastreamento permanecerá intacto."
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"message.save-success": [
|
"message.save-success": [
|
||||||
|
|
@ -610,7 +610,7 @@
|
||||||
"message.toggle-charts": [
|
"message.toggle-charts": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Toggle charts"
|
"value": "Mostrar/Esconder gráficos"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"message.track-stats": [
|
"message.track-stats": [
|
||||||
|
|
@ -750,7 +750,7 @@
|
||||||
"metrics.languages": [
|
"metrics.languages": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Languages"
|
"value": "Idiomas"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"metrics.operating-systems": [
|
"metrics.operating-systems": [
|
||||||
|
|
|
||||||
|
|
@ -176,7 +176,7 @@
|
||||||
"label.language": [
|
"label.language": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Language"
|
"value": "Язык"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.last-days": [
|
"label.last-days": [
|
||||||
|
|
@ -334,7 +334,7 @@
|
||||||
"label.theme": [
|
"label.theme": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Theme"
|
"value": "Тема"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.this-month": [
|
"label.this-month": [
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@
|
||||||
"label.administrator": [
|
"label.administrator": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Quản Trị"
|
"value": "Quản trị"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.all": [
|
"label.all": [
|
||||||
|
|
@ -32,19 +32,19 @@
|
||||||
"label.all-events": [
|
"label.all-events": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Tất cả events"
|
"value": "Tất cả sự kiện"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.all-time": [
|
"label.all-time": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "All time"
|
"value": "Toàn thời gian"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.all-websites": [
|
"label.all-websites": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Tất cả websites"
|
"value": "Tất cả website"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.back": [
|
"label.back": [
|
||||||
|
|
@ -104,7 +104,7 @@
|
||||||
"label.default-date-range": [
|
"label.default-date-range": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Phạm vi ngày mặc định"
|
"value": "Khoảng thời gian mặc định"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.delete": [
|
"label.delete": [
|
||||||
|
|
@ -242,7 +242,7 @@
|
||||||
"label.owner": [
|
"label.owner": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Owner"
|
"value": "Chủ nhân"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.password": [
|
"label.password": [
|
||||||
|
|
@ -590,7 +590,7 @@
|
||||||
"message.toggle-charts": [
|
"message.toggle-charts": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Toggle charts"
|
"value": "Bật/tắt biểu đồ"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"message.track-stats": [
|
"message.track-stats": [
|
||||||
|
|
@ -730,7 +730,7 @@
|
||||||
"metrics.languages": [
|
"metrics.languages": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Languages"
|
"value": "Ngôn ngũ"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"metrics.operating-systems": [
|
"metrics.operating-systems": [
|
||||||
|
|
@ -760,7 +760,7 @@
|
||||||
"metrics.unique-visitors": [
|
"metrics.unique-visitors": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Khách truy cập duy nhất"
|
"value": "Khách truy cập một lần"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"metrics.views": [
|
"metrics.views": [
|
||||||
|
|
|
||||||
100
scripts/check-db.js
Normal file
100
scripts/check-db.js
Normal file
|
|
@ -0,0 +1,100 @@
|
||||||
|
require('dotenv').config();
|
||||||
|
const { PrismaClient } = require('@prisma/client');
|
||||||
|
const prisma = new PrismaClient();
|
||||||
|
const chalk = require('chalk');
|
||||||
|
const spawn = require('cross-spawn');
|
||||||
|
|
||||||
|
let message = '';
|
||||||
|
const updateMessage = `To update your database, you need to run:\n${chalk.bold.whiteBright(
|
||||||
|
'yarn update-db',
|
||||||
|
)}`;
|
||||||
|
const baselineMessage = cmd =>
|
||||||
|
`You need to update your database by running:\n${chalk.bold.whiteBright(cmd)}`;
|
||||||
|
|
||||||
|
function success(msg) {
|
||||||
|
console.log(chalk.greenBright(`✓ ${msg}`));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function checkEnv() {
|
||||||
|
if (!process.env.DATABASE_URL) {
|
||||||
|
throw new Error('DATABASE_URL is not defined.');
|
||||||
|
} else {
|
||||||
|
success('DATABASE_URL is defined.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function checkConnection() {
|
||||||
|
try {
|
||||||
|
await prisma.$connect();
|
||||||
|
|
||||||
|
success('Database connection successful.');
|
||||||
|
} catch (e) {
|
||||||
|
throw new Error('Unable to connect to the database.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function checkTables() {
|
||||||
|
try {
|
||||||
|
await prisma.account.findFirst();
|
||||||
|
|
||||||
|
success('Database tables found.');
|
||||||
|
} catch (e) {
|
||||||
|
message = updateMessage;
|
||||||
|
|
||||||
|
throw new Error('Database tables not found.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function run(cmd, args) {
|
||||||
|
const buffer = [];
|
||||||
|
const proc = spawn(cmd, args);
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
proc.stdout.on('data', data => buffer.push(data));
|
||||||
|
|
||||||
|
proc.on('error', () => {
|
||||||
|
reject(new Error('Failed to run Prisma.'));
|
||||||
|
});
|
||||||
|
|
||||||
|
proc.on('exit', () => resolve(buffer.join('')));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function checkMigrations() {
|
||||||
|
const output = await run('prisma', ['migrate', 'status']);
|
||||||
|
|
||||||
|
const missingMigrations = output.includes('Following migration have not yet been applied');
|
||||||
|
const notManaged = output.includes('The current database is not managed');
|
||||||
|
|
||||||
|
if (notManaged) {
|
||||||
|
const cmd = output.match(/yarn prisma migrate resolve --applied ".*"/g);
|
||||||
|
|
||||||
|
message = baselineMessage(cmd[0]);
|
||||||
|
|
||||||
|
throw new Error('Database is out of date.');
|
||||||
|
} else if (missingMigrations) {
|
||||||
|
message = updateMessage;
|
||||||
|
|
||||||
|
throw new Error('Database is out of date.');
|
||||||
|
}
|
||||||
|
|
||||||
|
success('Database is up to date.');
|
||||||
|
}
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
let err = false;
|
||||||
|
for (let fn of [checkEnv, checkConnection, checkTables, checkMigrations]) {
|
||||||
|
try {
|
||||||
|
await fn();
|
||||||
|
} catch (e) {
|
||||||
|
console.log(chalk.red(`✗ ${e.message}`));
|
||||||
|
err = true;
|
||||||
|
} finally {
|
||||||
|
await prisma.$disconnect();
|
||||||
|
if (err) {
|
||||||
|
console.log(message);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
@ -1,8 +1,9 @@
|
||||||
require('dotenv').config();
|
require('dotenv').config();
|
||||||
const fs = require('fs');
|
const fse = require('fs-extra');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
|
const del = require('del');
|
||||||
|
|
||||||
function getDatabase() {
|
function getDatabaseType() {
|
||||||
const type =
|
const type =
|
||||||
process.env.DATABASE_TYPE ||
|
process.env.DATABASE_TYPE ||
|
||||||
(process.env.DATABASE_URL && process.env.DATABASE_URL.split(':')[0]);
|
(process.env.DATABASE_URL && process.env.DATABASE_URL.split(':')[0]);
|
||||||
|
|
@ -14,7 +15,7 @@ function getDatabase() {
|
||||||
return type;
|
return type;
|
||||||
}
|
}
|
||||||
|
|
||||||
const databaseType = getDatabase();
|
const databaseType = getDatabaseType();
|
||||||
|
|
||||||
if (!databaseType || !['mysql', 'postgresql'].includes(databaseType)) {
|
if (!databaseType || !['mysql', 'postgresql'].includes(databaseType)) {
|
||||||
throw new Error('Missing or invalid database');
|
throw new Error('Missing or invalid database');
|
||||||
|
|
@ -22,9 +23,11 @@ if (!databaseType || !['mysql', 'postgresql'].includes(databaseType)) {
|
||||||
|
|
||||||
console.log(`Database type detected: ${databaseType}`);
|
console.log(`Database type detected: ${databaseType}`);
|
||||||
|
|
||||||
const src = path.resolve(__dirname, `../prisma/schema.${databaseType}.prisma`);
|
const src = path.resolve(__dirname, `../db/${databaseType}`);
|
||||||
const dest = path.resolve(__dirname, '../prisma/schema.prisma');
|
const dest = path.resolve(__dirname, '../prisma');
|
||||||
|
|
||||||
fs.copyFileSync(src, dest);
|
del.sync(dest);
|
||||||
|
|
||||||
|
fse.copySync(src, dest);
|
||||||
|
|
||||||
console.log(`Copied ${src} to ${dest}`);
|
console.log(`Copied ${src} to ${dest}`);
|
||||||
|
|
@ -4,7 +4,7 @@ const https = require('https');
|
||||||
const chalk = require('chalk');
|
const chalk = require('chalk');
|
||||||
|
|
||||||
const src = path.resolve(__dirname, '../lang');
|
const src = path.resolve(__dirname, '../lang');
|
||||||
const dest = path.resolve(__dirname, '../public/country');
|
const dest = path.resolve(__dirname, '../public/intl/country');
|
||||||
const files = fs.readdirSync(src);
|
const files = fs.readdirSync(src);
|
||||||
|
|
||||||
const getUrl = locale =>
|
const getUrl = locale =>
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ const https = require('https');
|
||||||
const chalk = require('chalk');
|
const chalk = require('chalk');
|
||||||
|
|
||||||
const src = path.resolve(__dirname, '../lang');
|
const src = path.resolve(__dirname, '../lang');
|
||||||
const dest = path.resolve(__dirname, '../public/language');
|
const dest = path.resolve(__dirname, '../public/intl/language');
|
||||||
const files = fs.readdirSync(src);
|
const files = fs.readdirSync(src);
|
||||||
|
|
||||||
const getUrl = locale =>
|
const getUrl = locale =>
|
||||||
|
|
|
||||||
|
|
@ -71,4 +71,4 @@ create index event_created_at_idx on event(created_at);
|
||||||
create index event_website_id_idx on event(website_id);
|
create index event_website_id_idx on event(website_id);
|
||||||
create index event_session_id_idx on event(session_id);
|
create index event_session_id_idx on event(session_id);
|
||||||
|
|
||||||
insert into account (username, password, is_admin) values ('admin', '$2b$10$BUli0c.muyCW1ErNJc3jL.vFRFtFJWrT8/GcR4A.sUdCznaXiqFXa', true);
|
insert into account (username, password, is_admin) values ('admin', '$2b$10$BUli0c.muyCW1ErNJc3jL.vFRFtFJWrT8/GcR4A.sUdCznaXiqFXa', true);
|
||||||
|
|
|
||||||
|
|
@ -4,12 +4,13 @@ import semver from 'semver';
|
||||||
import { VERSION_CHECK } from 'lib/constants';
|
import { VERSION_CHECK } from 'lib/constants';
|
||||||
import { getItem } from 'lib/web';
|
import { getItem } from 'lib/web';
|
||||||
|
|
||||||
const REPO_URL = 'https://api.github.com/repos/mikecao/umami/releases/latest';
|
const UPDATES_URL = 'https://api.umami.is/v1/updates';
|
||||||
|
|
||||||
const initialState = {
|
const initialState = {
|
||||||
current: process.env.VERSION,
|
current: process.env.currentVersion,
|
||||||
latest: null,
|
latest: null,
|
||||||
hasUpdate: false,
|
hasUpdate: false,
|
||||||
|
checked: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
const store = create(() => ({ ...initialState }));
|
const store = create(() => ({ ...initialState }));
|
||||||
|
|
@ -17,10 +18,10 @@ const store = create(() => ({ ...initialState }));
|
||||||
export async function checkVersion() {
|
export async function checkVersion() {
|
||||||
const { current } = store.getState();
|
const { current } = store.getState();
|
||||||
|
|
||||||
const data = await fetch(REPO_URL, {
|
const data = await fetch(`${UPDATES_URL}?v=${current}`, {
|
||||||
method: 'get',
|
method: 'GET',
|
||||||
headers: {
|
headers: {
|
||||||
Accept: 'application/vnd.github.v3+json',
|
Accept: 'application/json',
|
||||||
},
|
},
|
||||||
}).then(res => {
|
}).then(res => {
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
|
|
@ -36,15 +37,15 @@ export async function checkVersion() {
|
||||||
|
|
||||||
store.setState(
|
store.setState(
|
||||||
produce(state => {
|
produce(state => {
|
||||||
const { tag_name } = data;
|
const { latest } = data;
|
||||||
|
|
||||||
const latest = tag_name.startsWith('v') ? tag_name.slice(1) : tag_name;
|
|
||||||
const lastCheck = getItem(VERSION_CHECK);
|
const lastCheck = getItem(VERSION_CHECK);
|
||||||
const hasUpdate = latest && semver.gt(latest, current) && lastCheck?.version !== latest;
|
|
||||||
|
const hasUpdate = !!(latest && lastCheck?.version !== latest && semver.gt(latest, current));
|
||||||
|
|
||||||
state.current = current;
|
state.current = current;
|
||||||
state.latest = latest;
|
state.latest = latest;
|
||||||
state.hasUpdate = hasUpdate;
|
state.hasUpdate = hasUpdate;
|
||||||
|
state.checked = true;
|
||||||
|
|
||||||
return state;
|
return state;
|
||||||
}),
|
}),
|
||||||
|
|
|
||||||
|
|
@ -122,7 +122,11 @@ import { removeTrailingSlash } from '../lib/url';
|
||||||
payload,
|
payload,
|
||||||
});
|
});
|
||||||
|
|
||||||
navigator.sendBeacon(`${root}/api/collect`, data);
|
fetch(`${root}/api/collect`, {
|
||||||
|
method: 'POST',
|
||||||
|
body: data,
|
||||||
|
keepalive: true,
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const addEvents = node => {
|
const addEvents = node => {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue