Merge pull request #916 from mikecao/dev

v1.25.0
This commit is contained in:
Mike Cao 2022-01-18 01:35:11 -08:00 committed by GitHub
commit 5d74e86222
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
108 changed files with 876 additions and 379 deletions

View File

@ -24,6 +24,7 @@ function Button({
data-tip={tooltip}
data-effect="solid"
data-for={tooltipId}
data-offset={JSON.stringify({ left: 10 })}
type={type}
className={classNames(styles.button, className, {
[styles.large]: size === 'large',

View File

@ -106,9 +106,14 @@ export default function Calendar({ date, minDate, maxDate, onChange }) {
}
const DaySelector = ({ date, minDate, maxDate, locale, onSelect }) => {
const startWeek = startOfWeek(date, { locale: getDateLocale(locale) });
const startMonth = startOfMonth(date, { locale: getDateLocale(locale) });
const startDay = subDays(startMonth, startMonth.getDay());
const dateLocale = getDateLocale(locale);
const weekStartsOn = dateLocale?.options?.weekStartsOn || 0;
const startWeek = startOfWeek(date, {
locale: dateLocale,
weekStartsOn,
});
const startMonth = startOfMonth(date);
const startDay = subDays(startMonth, startMonth.getDay() - weekStartsOn);
const month = date.getMonth();
const year = date.getFullYear();

View File

@ -6,7 +6,7 @@ import Modal from './Modal';
import DropDown from './DropDown';
import DatePickerForm from 'components/forms/DatePickerForm';
import useLocale from 'hooks/useLocale';
import { getDateRange, dateFormat } from 'lib/date';
import { dateFormat } from 'lib/date';
import Calendar from 'assets/calendar-alt.svg';
import Icon from './Icon';
@ -47,6 +47,11 @@ const filterOptions = [
value: '90day',
},
{ label: <FormattedMessage id="label.this-year" defaultMessage="This year" />, value: '1year' },
{
label: <FormattedMessage id="label.all-time" defaultMessage="All time" />,
value: 'all',
divider: true,
},
{
label: <FormattedMessage id="label.custom-range" defaultMessage="Custom range" />,
value: 'custom',
@ -55,7 +60,6 @@ const filterOptions = [
];
function DateFilter({ value, startDate, endDate, onChange, className }) {
const { locale } = useLocale();
const [showPicker, setShowPicker] = useState(false);
const displayValue =
value === 'custom' ? (
@ -64,12 +68,12 @@ function DateFilter({ value, startDate, endDate, onChange, className }) {
value
);
function handleChange(value) {
async function handleChange(value) {
if (value === 'custom') {
setShowPicker(true);
return;
}
onChange(getDateRange(value, locale));
onChange(value);
}
function handlePickerChange(value) {

View File

@ -7,12 +7,9 @@ import Button from './Button';
import Refresh from 'assets/redo.svg';
import Dots from 'assets/ellipsis-h.svg';
import useDateRange from 'hooks/useDateRange';
import { getDateRange } from '../../lib/date';
import useLocale from 'hooks/useLocale';
function RefreshButton({ websiteId }) {
const dispatch = useDispatch();
const { locale } = useLocale();
const [dateRange] = useDateRange(websiteId);
const [loading, setLoading] = useState(false);
const completed = useSelector(state => state.queries[`/api/website/${websiteId}/stats`]);
@ -20,7 +17,7 @@ function RefreshButton({ websiteId }) {
function handleClick() {
if (dateRange) {
setLoading(true);
dispatch(setDateRange(websiteId, getDateRange(dateRange.value, locale)));
dispatch(setDateRange(websiteId, dateRange));
}
}

View File

@ -4,7 +4,7 @@ import PropTypes from 'prop-types';
import ReactTooltip from 'react-tooltip';
import { ComposableMap, Geographies, Geography, ZoomableGroup } from 'react-simple-maps';
import classNames from 'classnames';
import tinycolor from 'tinycolor2';
import { colord } from 'colord';
import useTheme from 'hooks/useTheme';
import { ISO_COUNTRIES, THEME_COLORS, MAP_FILE } from 'lib/constants';
import styles from './WorldMap.module.css';
@ -35,9 +35,9 @@ function WorldMap({ data, className }) {
return colors.fillColor;
}
return tinycolor(colors.baseColor)[theme === 'light' ? 'lighten' : 'darken'](
40 * (1.0 - country.z / 100),
);
return colord(colors.baseColor)
[theme === 'light' ? 'lighten' : 'darken'](0.4 * (1.0 - country.z / 100))
.toHex();
}
function getOpacity(code) {

View File

@ -6,10 +6,10 @@ import { formatLongNumber } from 'lib/format';
import { dateFormat } from 'lib/date';
import useLocale from 'hooks/useLocale';
import useTheme from 'hooks/useTheme';
import useForceUpdate from 'hooks/useForceUpdate';
import { DEFAUL_CHART_HEIGHT, DEFAULT_ANIMATION_DURATION, THEME_COLORS } from 'lib/constants';
import styles from './BarChart.module.css';
import ChartTooltip from './ChartTooltip';
import useForceUpdate from '../../hooks/useForceUpdate';
export default function BarChart({
chartId,

View File

@ -1,5 +1,5 @@
import React, { useMemo } from 'react';
import tinycolor from 'tinycolor2';
import { colord } from 'colord';
import BarChart from './BarChart';
import { getDateArray, getDateLength } from 'lib/date';
import useFetch from 'hooks/useFetch';
@ -51,13 +51,13 @@ export default function EventsChart({ websiteId, className, token }) {
});
return Object.keys(map).map((key, index) => {
const color = tinycolor(EVENT_COLORS[index % EVENT_COLORS.length]);
const color = colord(EVENT_COLORS[index % EVENT_COLORS.length]);
return {
label: key,
data: map[key],
lineTension: 0,
backgroundColor: color.setAlpha(0.6).toRgbString(),
borderColor: color.setAlpha(0.7).toRgbString(),
backgroundColor: color.alpha(0.6).toRgbString(),
borderColor: color.alpha(0.7).toRgbString(),
borderWidth: 1,
};
});

View File

@ -0,0 +1,31 @@
import React from 'react';
import MetricsTable from './MetricsTable';
import { percentFilter } from 'lib/filters';
import { FormattedMessage } from 'react-intl';
import useLanguageNames from 'hooks/useLanguageNames';
import useLocale from 'hooks/useLocale';
export default function LanguagesTable({ websiteId, onDataLoad, ...props }) {
const { locale } = useLocale();
const languageNames = useLanguageNames(locale);
function renderLabel({ x }) {
return (
<div className={locale}>
{languageNames[x] ?? <FormattedMessage id="label.unknown" defaultMessage="Unknown" />}{' '}
</div>
);
}
return (
<MetricsTable
{...props}
title={<FormattedMessage id="metrics.languages" defaultMessage="Languages" />}
type="language"
metric={<FormattedMessage id="metrics.visitors" defaultMessage="Visitors" />}
websiteId={websiteId}
onDataLoad={data => onDataLoad?.(percentFilter(data))}
renderLabel={renderLabel}
/>
);
}

View File

@ -1,9 +1,10 @@
import React from 'react';
import { colord } from 'colord';
import classNames from 'classnames';
import Dot from 'components/common/Dot';
import useLocale from 'hooks/useLocale';
import useForceUpdate from 'hooks/useForceUpdate';
import styles from './Legend.module.css';
import useForceUpdate from '../../hooks/useForceUpdate';
export default function Legend({ chart }) {
const { locale } = useLocale();
@ -25,16 +26,20 @@ export default function Legend({ chart }) {
return (
<div className={styles.legend}>
{chart.legend.legendItems.map(({ text, fillStyle, datasetIndex, hidden }) => (
<div
key={text}
className={classNames(styles.label, { [styles.hidden]: hidden })}
onClick={() => handleClick(datasetIndex)}
>
<Dot color={fillStyle} />
<span className={locale}>{text}</span>
</div>
))}
{chart.legend.legendItems.map(({ text, fillStyle, datasetIndex, hidden }) => {
const color = colord(fillStyle);
return (
<div
key={text}
className={classNames(styles.label, { [styles.hidden]: hidden })}
onClick={() => handleClick(datasetIndex)}
>
<Dot color={color.alpha(color.alpha() + 0.2).toHex()} />
<span className={locale}>{text}</span>
</div>
);
})}
</div>
);
}

View File

@ -9,6 +9,7 @@ const MetricCard = ({
label,
reverseColors = false,
format = formatNumber,
hideComparison = false,
}) => {
const props = useSpring({ x: Number(value) || 0, from: { x: 0 } });
const changeProps = useSpring({ x: Number(change) || 0, from: { x: 0 } });
@ -18,8 +19,8 @@ const MetricCard = ({
<animated.div className={styles.value}>{props.x.interpolate(x => format(x))}</animated.div>
<div className={styles.label}>
{label}
{~~change === 0 && <span className={styles.change}>{format(0)}</span>}
{~~change !== 0 && (
{~~change === 0 && !hideComparison && <span className={styles.change}>{format(0)}</span>}
{~~change !== 0 && !hideComparison && (
<animated.span
className={`${styles.change} ${
change >= 0

View File

@ -1,6 +1,6 @@
import React from 'react';
import { useIntl } from 'react-intl';
import tinycolor from 'tinycolor2';
import { colord } from 'colord';
import CheckVisible from 'components/helpers/CheckVisible';
import BarChart from './BarChart';
import useTheme from 'hooks/useTheme';
@ -18,15 +18,15 @@ export default function PageviewsChart({
}) {
const intl = useIntl();
const [theme] = useTheme();
const primaryColor = tinycolor(THEME_COLORS[theme].primary);
const primaryColor = colord(THEME_COLORS[theme].primary);
const colors = {
views: {
background: primaryColor.setAlpha(0.4).toRgbString(),
border: primaryColor.setAlpha(0.5).toRgbString(),
background: primaryColor.alpha(0.4).toRgbString(),
border: primaryColor.alpha(0.5).toRgbString(),
},
visitors: {
background: primaryColor.setAlpha(0.6).toRgbString(),
border: primaryColor.setAlpha(0.7).toRgbString(),
background: primaryColor.alpha(0.6).toRgbString(),
border: primaryColor.alpha(0.7).toRgbString(),
},
};

View File

@ -2,6 +2,7 @@ import React from 'react';
import { FormattedMessage } from 'react-intl';
import PageHeader from '../layout/PageHeader';
import DropDown from '../common/DropDown';
import ActiveUsers from './ActiveUsers';
import MetricCard from './MetricCard';
import styles from './RealtimeHeader.module.css';
@ -24,24 +25,31 @@ export default function RealtimeHeader({ websites, data, websiteId, onSelect })
<div>
<FormattedMessage id="label.realtime" defaultMessage="Realtime" />
</div>
<div>
<ActiveUsers className={styles.active} websiteId={websiteId} />
</div>
<DropDown value={websiteId} options={options} onChange={onSelect} />
</PageHeader>
<div className={styles.metrics}>
<MetricCard
label={<FormattedMessage id="metrics.views" defaultMessage="Views" />}
value={pageviews.length}
hideComparison
/>
<MetricCard
label={<FormattedMessage id="metrics.visitors" defaultMessage="Visitors" />}
value={sessions.length}
hideComparison
/>
<MetricCard
label={<FormattedMessage id="metrics.events" defaultMessage="Events" />}
value={events.length}
hideComparison
/>
<MetricCard
label={<FormattedMessage id="metrics.countries" defaultMessage="Countries" />}
value={countries.length}
hideComparison
/>
</div>
</>

View File

@ -7,7 +7,7 @@ import Tag from 'components/common/Tag';
import Dot from 'components/common/Dot';
import FilterButtons from 'components/common/FilterButtons';
import NoData from 'components/common/NoData';
import { devices } from 'components/messages';
import { getDeviceMessage } from 'components/messages';
import useLocale from 'hooks/useLocale';
import useCountryNames from 'hooks/useCountryNames';
import { BROWSERS } from 'lib/constants';
@ -137,7 +137,7 @@ export default function RealtimeLog({ data, websites, websiteId }) {
),
browser: <b>{BROWSERS[browser]}</b>,
os: <b>{os}</b>,
device: <b>{intl.formatMessage(devices[device])?.toLowerCase()}</b>,
device: <b>{getDeviceMessage(device)}</b>,
}}
/>
);

View File

@ -35,7 +35,6 @@ export default function ReferrersTable({ websiteId, websiteDomain, showFilters,
];
const renderLink = ({ w: link, x: label }) => {
console.log({ link, label });
return (
<div className={styles.row}>
<Link href={resolve({ ref: label })} replace={true}>

View File

@ -1,19 +1,22 @@
import React, { useMemo } from 'react';
import classNames from 'classnames';
import { useRouter } from 'next/router';
import PageviewsChart from './PageviewsChart';
import MetricsBar from './MetricsBar';
import WebsiteHeader from './WebsiteHeader';
import DateFilter from 'components/common/DateFilter';
import StickyHeader from 'components/helpers/StickyHeader';
import ErrorMessage from 'components/common/ErrorMessage';
import FilterTags from 'components/metrics/FilterTags';
import useFetch from 'hooks/useFetch';
import useDateRange from 'hooks/useDateRange';
import useTimezone from 'hooks/useTimezone';
import usePageQuery from 'hooks/usePageQuery';
import { getDateArray, getDateLength } from 'lib/date';
import ErrorMessage from 'components/common/ErrorMessage';
import FilterTags from 'components/metrics/FilterTags';
import useLocale from 'hooks/useLocale';
import { getDateArray, getDateLength, getDateRange, getDateRangeValues } from 'lib/date';
import useShareToken from 'hooks/useShareToken';
import { TOKEN_HEADER } from 'lib/constants';
import { get } from 'lib/web';
import styles from './WebsiteChart.module.css';
export default function WebsiteChart({
@ -22,13 +25,15 @@ export default function WebsiteChart({
domain,
stickyHeader = false,
showLink = false,
hideChart = false,
showChart = true,
onDataLoad = () => {},
}) {
const shareToken = useShareToken();
const [dateRange, setDateRange] = useDateRange(websiteId);
const { startDate, endDate, unit, value, modified } = dateRange;
const { locale } = useLocale();
const [timezone] = useTimezone();
const { basePath } = useRouter();
const {
router,
resolve,
@ -66,6 +71,17 @@ export default function WebsiteChart({
router.push(resolve({ [param]: undefined }));
}
async function handleDateChange(value) {
if (value === 'all') {
const { data, ok } = await get(`${basePath}/api/website/${websiteId}`);
if (ok) {
setDateRange({ value, ...getDateRangeValues(new Date(data.created_at), Date.now()) });
}
} else {
setDateRange(getDateRange(value, locale));
}
}
return (
<div className={styles.container}>
<WebsiteHeader websiteId={websiteId} title={title} domain={domain} showLink={showLink} />
@ -84,7 +100,7 @@ export default function WebsiteChart({
value={value}
startDate={startDate}
endDate={endDate}
onChange={setDateRange}
onChange={handleDateChange}
/>
</div>
</StickyHeader>
@ -92,7 +108,7 @@ export default function WebsiteChart({
<div className="row">
<div className={classNames(styles.chart, 'col')}>
{error && <ErrorMessage />}
{!hideChart && (
{showChart && (
<PageviewsChart
websiteId={websiteId}
data={chartData}

View File

@ -4,19 +4,22 @@ import classNames from 'classnames';
import Head from 'next/head';
import Link from 'next/link';
import { useRouter } from 'next/router';
import Page from '../layout/Page';
import PageHeader from '../layout/PageHeader';
import useFetch from '../../hooks/useFetch';
import DropDown from '../common/DropDown';
import Page from 'components/layout/Page';
import PageHeader from 'components/layout/PageHeader';
import DropDown from 'components/common/DropDown';
import WebsiteChart from 'components/metrics/WebsiteChart';
import EventsChart from 'components/metrics/EventsChart';
import Button from 'components/common/Button';
import EmptyPlaceholder from 'components/common/EmptyPlaceholder';
import Icon from 'components/common/Icon';
import useFetch from 'hooks/useFetch';
import ChevronDown from 'assets/chevron-down.svg';
import styles from './TestConsole.module.css';
import WebsiteChart from '../metrics/WebsiteChart';
import EventsChart from '../metrics/EventsChart';
import Button from '../common/Button';
import EmptyPlaceholder from '../common/EmptyPlaceholder';
export default function TestConsole() {
const user = useSelector(state => state.user);
const [website, setWebsite] = useState();
const [show, setShow] = useState(true);
const { basePath } = useRouter();
const { data } = useFetch('/api/websites');
@ -55,33 +58,46 @@ export default function TestConsole() {
{!selectedValue && <EmptyPlaceholder msg="I hope you know what you're doing here" />}
{selectedValue && (
<>
<div className={classNames(styles.test, 'row')}>
<div className="col-4">
<PageHeader>Page links</PageHeader>
<div>
<Link href={`?page=1`}>
<a>page one</a>
</Link>
</div>
<div>
<Link href={`?page=2`}>
<a>page two</a>
</Link>
</div>
</div>
<div className="col-4">
<PageHeader>CSS events</PageHeader>
<Button id="primary-button" className="umami--click--primary-button" variant="action">
Send event
</Button>
</div>
<div className="col-4">
<PageHeader>Javascript events</PageHeader>
<Button id="manual-button" variant="action" onClick={handleClick}>
Run script
</Button>
</div>
<div>
<Icon
icon={<ChevronDown />}
className={classNames({ [styles.hidden]: !show })}
onClick={() => setShow(!show)}
/>
</div>
{show && (
<div className={classNames(styles.test, 'row')}>
<div className="col-4">
<PageHeader>Page links</PageHeader>
<div>
<Link href={`?page=1`}>
<a>page one</a>
</Link>
</div>
<div>
<Link href={`?page=2`}>
<a>page two</a>
</Link>
</div>
</div>
<div className="col-4">
<PageHeader>CSS events</PageHeader>
<Button
id="primary-button"
className="umami--click--primary-button"
variant="action"
>
Send event
</Button>
</div>
<div className="col-4">
<PageHeader>Javascript events</PageHeader>
<Button id="manual-button" variant="action" onClick={handleClick}>
Run script
</Button>
</div>
</div>
)}
<div className="row">
<div className="col-12">
<WebsiteChart

View File

@ -3,3 +3,7 @@
border-radius: 5px;
padding: 0 20px 20px 20px;
}
.hidden {
transform: rotate(-90deg);
}

View File

@ -16,6 +16,7 @@ import BrowsersTable from '../metrics/BrowsersTable';
import OSTable from '../metrics/OSTable';
import DevicesTable from '../metrics/DevicesTable';
import CountriesTable from '../metrics/CountriesTable';
import LanguagesTable from '../metrics/LanguagesTable';
import EventsTable from '../metrics/EventsTable';
import EventsChart from '../metrics/EventsChart';
import useFetch from 'hooks/useFetch';
@ -30,6 +31,7 @@ const views = {
os: OSTable,
device: DevicesTable,
country: CountriesTable,
language: LanguagesTable,
event: EventsTable,
};
@ -82,6 +84,10 @@ export default function WebsiteDetails({ websiteId }) {
label: <FormattedMessage id="metrics.countries" defaultMessage="Countries" />,
value: resolve({ view: 'country' }),
},
{
label: <FormattedMessage id="metrics.languages" defaultMessage="Languages" />,
value: resolve({ view: 'language' }),
},
{
label: <FormattedMessage id="metrics.events" defaultMessage="Events" />,
value: resolve({ view: 'event' }),

View File

@ -12,7 +12,7 @@ import styles from './WebsiteList.module.css';
export default function WebsiteList({ userId }) {
const { data } = useFetch('/api/websites', { params: { user_id: userId } });
const [hideCharts, setHideCharts] = useState(false);
const [showCharts, setShowCharts] = useState(true);
if (!data) {
return null;
@ -43,7 +43,7 @@ export default function WebsiteList({ userId }) {
<Button
tooltip={<FormattedMessage id="message.toggle-charts" defaultMessage="Toggle charts" />}
icon={<Chart />}
onClick={() => setHideCharts(!hideCharts)}
onClick={() => setShowCharts(!showCharts)}
/>
</div>
{data.map(({ website_id, name, domain }) => (
@ -52,7 +52,7 @@ export default function WebsiteList({ userId }) {
websiteId={website_id}
title={name}
domain={domain}
hideChart={hideCharts}
showChart={showCharts}
showLink
/>
</div>

View File

@ -1,5 +1,6 @@
import React, { useState } from 'react';
import { FormattedMessage } from 'react-intl';
import { useSelector } from 'react-redux';
import classNames from 'classnames';
import Link from 'components/common/Link';
import Table from 'components/common/Table';
@ -25,6 +26,7 @@ import useFetch from 'hooks/useFetch';
import styles from './WebsiteSettings.module.css';
export default function WebsiteSettings() {
const user = useSelector(state => state.user);
const [editWebsite, setEditWebsite] = useState();
const [resetWebsite, setResetWebsite] = useState();
const [deleteWebsite, setDeleteWebsite] = useState();
@ -33,7 +35,9 @@ export default function WebsiteSettings() {
const [showUrl, setShowUrl] = useState();
const [saved, setSaved] = useState(0);
const [message, setMessage] = useState();
const { data } = useFetch(`/api/websites`, {}, [saved]);
const { data } = useFetch(`/api/websites` + (user.is_admin ? '?include_all=true' : ''), {}, [
saved,
]);
const Buttons = row => (
<ButtonLayout align="right">
@ -55,15 +59,27 @@ export default function WebsiteSettings() {
tooltipId={`button-code-${row.website_id}`}
onClick={() => setShowCode(row)}
/>
<Button icon={<Pen />} size="small" onClick={() => setEditWebsite(row)}>
<FormattedMessage id="label.edit" defaultMessage="Edit" />
</Button>
<Button icon={<Reset />} size="small" onClick={() => setResetWebsite(row)}>
<FormattedMessage id="label.reset" defaultMessage="Reset" />
</Button>
<Button icon={<Trash />} size="small" onClick={() => setDeleteWebsite(row)}>
<FormattedMessage id="label.delete" defaultMessage="Delete" />
</Button>
<Button
icon={<Pen />}
size="small"
tooltip={<FormattedMessage id="label.edit" defaultMessage="Edit" />}
tooltipId={`button-edit-${row.website_id}`}
onClick={() => setEditWebsite(row)}
/>
<Button
icon={<Reset />}
size="small"
tooltip={<FormattedMessage id="label.reset" defaultMessage="Reset" />}
tooltipId={`button-reset-${row.website_id}`}
onClick={() => setResetWebsite(row)}
/>
<Button
icon={<Trash />}
size="small"
tooltip={<FormattedMessage id="label.delete" defaultMessage="Delete" />}
tooltipId={`button-delete-${row.website_id}`}
onClick={() => setDeleteWebsite(row)}
/>
</ButtonLayout>
);
@ -74,6 +90,30 @@ export default function WebsiteSettings() {
</Link>
);
const adminColumns = [
{
key: 'name',
label: <FormattedMessage id="label.name" defaultMessage="Name" />,
className: 'col-4 col-xl-3',
render: DetailsLink,
},
{
key: 'domain',
label: <FormattedMessage id="label.domain" defaultMessage="Domain" />,
className: 'col-4 col-xl-3',
},
{
key: 'account',
label: <FormattedMessage id="label.owner" defaultMessage="Owner" />,
className: 'col-4 col-xl-1',
},
{
key: 'action',
className: classNames(styles.buttons, 'col-12 col-xl-5 pt-2 pt-xl-0'),
render: Buttons,
},
];
const columns = [
{
key: 'name',
@ -137,7 +177,7 @@ export default function WebsiteSettings() {
<FormattedMessage id="label.add-website" defaultMessage="Add website" />
</Button>
</PageHeader>
<Table columns={columns} rows={data} empty={empty} />
<Table columns={user.is_admin ? adminColumns : columns} rows={data} empty={empty} />
{editWebsite && (
<Modal title={<FormattedMessage id="label.edit-website" defaultMessage="Edit website" />}>
<WebsiteEditForm values={editWebsite} onSave={handleSave} onClose={handleClose} />

View File

@ -1,8 +1,8 @@
import { useState, useEffect } from 'react';
import { useDispatch } from 'react-redux';
import { useRouter } from 'next/router';
import { get } from 'lib/web';
import { updateQuery } from 'redux/actions/queries';
import { useRouter } from 'next/router';
export default function useFetch(url, options = {}, update = []) {
const dispatch = useDispatch();

34
hooks/useLanguageNames.js Normal file
View File

@ -0,0 +1,34 @@
import { useState, useEffect } from 'react';
import { useRouter } from 'next/router';
import { get } from 'lib/web';
import enUS from 'public/language/en-US.json';
const languageNames = {
'en-US': enUS,
};
export default function useLanguageNames(locale) {
const [list, setList] = useState(languageNames[locale] || enUS);
const { basePath } = useRouter();
async function loadData(locale) {
const { ok, data } = await get(`${basePath}/language/${locale}.json`);
if (ok) {
languageNames[locale] = data;
setList(languageNames[locale]);
} else {
setList(enUS);
}
}
useEffect(() => {
if (!languageNames[locale]) {
loadData(locale);
} else {
setList(languageNames[locale]);
}
}, [locale]);
return list;
}

View File

@ -5,6 +5,7 @@
"label.administrator": "مدير عام؟",
"label.all": "الكل",
"label.all-events": "كافة الأحداث",
"label.all-time": "All time",
"label.all-websites": "كافة المواقع",
"label.back": "للخلف",
"label.cancel": "إلغاء",
@ -35,6 +36,7 @@
"label.more": "المزيد",
"label.name": "الإسم",
"label.new-password": "كلمة مرور جديدة",
"label.owner": "Owner",
"label.password": "كلمة المرور",
"label.passwords-dont-match": "كلمة المرور غير متطابقة",
"label.profile": "الملف الشخصي",
@ -95,6 +97,7 @@
"metrics.filter.combined": "مجمعة",
"metrics.filter.domain-only": "نطاق فقط",
"metrics.filter.raw": "مفصلة",
"metrics.languages": "Languages",
"metrics.operating-systems": "نظام التشغيل",
"metrics.page-views": "مشاهدات الصفحة",
"metrics.pages": "الصفحات",

View File

@ -5,6 +5,7 @@
"label.administrator": "Administrador",
"label.all": "Tots",
"label.all-events": "Tots els esdeveniments",
"label.all-time": "All time",
"label.all-websites": "Tots els llocs web",
"label.back": "Enrere",
"label.cancel": "Cancel·la",
@ -35,6 +36,7 @@
"label.more": "Més",
"label.name": "Nom",
"label.new-password": "Contrasenya nova",
"label.owner": "Owner",
"label.password": "Contrasenya",
"label.passwords-dont-match": "Les contrasenyes no coincideixen",
"label.profile": "Perfil",
@ -95,6 +97,7 @@
"metrics.filter.combined": "Combinat",
"metrics.filter.domain-only": "Només domini",
"metrics.filter.raw": "En cru",
"metrics.languages": "Languages",
"metrics.operating-systems": "Sistemes operatius",
"metrics.page-views": "Pàgines vistes",
"metrics.pages": "Pàgines",

View File

@ -5,6 +5,7 @@
"label.administrator": "Administrátor",
"label.all": "Vše",
"label.all-events": "All events",
"label.all-time": "All time",
"label.all-websites": "Všechny weby",
"label.back": "Zpět",
"label.cancel": "Zrušit",
@ -35,6 +36,7 @@
"label.more": "Více",
"label.name": "Jméno",
"label.new-password": "Nové heslo",
"label.owner": "Owner",
"label.password": "Heslo",
"label.passwords-dont-match": "Hesla se neschodují",
"label.profile": "Profil",
@ -95,6 +97,7 @@
"metrics.filter.combined": "Kombinace",
"metrics.filter.domain-only": "Domény",
"metrics.filter.raw": "Nezpracované",
"metrics.languages": "Languages",
"metrics.operating-systems": "Operační systém",
"metrics.page-views": "Zobrazení stránek",
"metrics.pages": "Stránky",

View File

@ -5,6 +5,7 @@
"label.administrator": "Administrator",
"label.all": "Alle",
"label.all-events": "All events",
"label.all-time": "All time",
"label.all-websites": "Alle websites",
"label.back": "Tilbage",
"label.cancel": "Afvis",
@ -35,6 +36,7 @@
"label.more": "Mere",
"label.name": "Navn",
"label.new-password": "Ny adgangskode",
"label.owner": "Owner",
"label.password": "Adgangskode",
"label.passwords-dont-match": "Adgangskoder matcher ikke",
"label.profile": "Profil",
@ -95,6 +97,7 @@
"metrics.filter.combined": "Kombineret",
"metrics.filter.domain-only": "Kun domæne",
"metrics.filter.raw": "Rå",
"metrics.languages": "Languages",
"metrics.operating-systems": "Operativsystemer",
"metrics.page-views": "Sidevisninger",
"metrics.pages": "Sider",

View File

@ -5,6 +5,7 @@
"label.administrator": "Administrator",
"label.all": "Alle",
"label.all-events": "Alle Ereignisse",
"label.all-time": "All time",
"label.all-websites": "Alle Webseiten",
"label.back": "Zurück",
"label.cancel": "Abbrechen",
@ -35,6 +36,7 @@
"label.more": "Mehr",
"label.name": "Name",
"label.new-password": "Neues Passwort",
"label.owner": "Owner",
"label.password": "Passwort",
"label.passwords-dont-match": "Passwörter stimmen nicht überein",
"label.profile": "Profil",
@ -43,7 +45,7 @@
"label.refresh": "Aktualisieren",
"label.required": "Erforderlich",
"label.reset": "Zurücksetzen",
"label.reset-website": "Reset statistics",
"label.reset-website": "Statistik zurücksetzen",
"label.save": "Speichern",
"label.settings": "Einstellungen",
"label.share-url": "Freigabe-URL",
@ -74,13 +76,13 @@
"message.no-websites-configured": "Es ist keine Webseite vorhanden.",
"message.page-not-found": "Seite nicht gefunden.",
"message.powered-by": "Betrieben durch {name}",
"message.reset-warning": "All statistics for this website will be deleted, but your tracking code will remain intact.",
"message.reset-warning": "Alle Daten für diese Website werden gelöscht, jedoch bleibt der tracking code bestehen.",
"message.save-success": "Erfolgreich gespeichert.",
"message.share-url": "Dies ist die öffentliche URL zum Teilen für {target}.",
"message.toggle-charts": "Toggle charts",
"message.toggle-charts": "Schaubilder umschalten",
"message.track-stats": "Um die Statistiken für {target} zu übermitteln, platzieren Sie bitte den folgenden Quelltext im {head} ihrer Webseite.",
"message.type-delete": "Geben Sie {delete} in das Feld unten ein um zu bestätigen.",
"message.type-reset": "Type {reset} in the box below to confirm.",
"message.type-reset": "Geben Sie {reset} in das Feld unten ein um zu bestätigen.",
"metrics.actions": "Aktionen",
"metrics.average-visit-time": "Durchschn. Besuchszeit",
"metrics.bounce-rate": "Absprungrate",
@ -95,6 +97,7 @@
"metrics.filter.combined": "Kombiniert",
"metrics.filter.domain-only": "Nur diese Domain",
"metrics.filter.raw": "Rohdaten",
"metrics.languages": "Languages",
"metrics.operating-systems": "Betriebssysteme",
"metrics.page-views": "Seitenaufrufe",
"metrics.pages": "Seiten",

View File

@ -5,6 +5,7 @@
"label.administrator": "Διαχειριστής",
"label.all": "All",
"label.all-events": "All events",
"label.all-time": "All time",
"label.all-websites": "All websites",
"label.back": "Πίσω",
"label.cancel": "Ακύρωση",
@ -35,6 +36,7 @@
"label.more": "Περισσότερα",
"label.name": "Όνομα",
"label.new-password": "Νέος κωδικός",
"label.owner": "Owner",
"label.password": "Κωδικός",
"label.passwords-dont-match": "Οι κωδικοί πρόσβασης δεν ταιριάζουν",
"label.profile": "Προφίλ",
@ -95,6 +97,7 @@
"metrics.filter.combined": "Σε συνδυασμό",
"metrics.filter.domain-only": "Μόνο τομέας",
"metrics.filter.raw": "Ακατέργαστο",
"metrics.languages": "Languages",
"metrics.operating-systems": "Λειτουργικά συστήματα",
"metrics.page-views": "Προβολές σελίδας",
"metrics.pages": "Σελίδες",

View File

@ -5,6 +5,7 @@
"label.administrator": "Administrator",
"label.all": "All",
"label.all-events": "All events",
"label.all-time": "All time",
"label.all-websites": "All websites",
"label.back": "Back",
"label.cancel": "Cancel",
@ -35,6 +36,7 @@
"label.more": "More",
"label.name": "Name",
"label.new-password": "New password",
"label.owner": "Owner",
"label.password": "Password",
"label.passwords-dont-match": "Passwords don't match",
"label.profile": "Profile",
@ -95,6 +97,7 @@
"metrics.filter.combined": "Combined",
"metrics.filter.domain-only": "Domain only",
"metrics.filter.raw": "Raw",
"metrics.languages": "Languages",
"metrics.operating-systems": "Operating systems",
"metrics.page-views": "Page views",
"metrics.pages": "Pages",

View File

@ -5,6 +5,7 @@
"label.administrator": "Administrator",
"label.all": "All",
"label.all-events": "All events",
"label.all-time": "All time",
"label.all-websites": "All websites",
"label.back": "Back",
"label.cancel": "Cancel",
@ -35,6 +36,7 @@
"label.more": "More",
"label.name": "Name",
"label.new-password": "New password",
"label.owner": "Owner",
"label.password": "Password",
"label.passwords-dont-match": "Passwords don't match",
"label.profile": "Profile",
@ -95,6 +97,7 @@
"metrics.filter.combined": "Combined",
"metrics.filter.domain-only": "Domain only",
"metrics.filter.raw": "Raw",
"metrics.languages": "Languages",
"metrics.operating-systems": "Operating systems",
"metrics.page-views": "Page views",
"metrics.pages": "Pages",

View File

@ -5,6 +5,7 @@
"label.administrator": "Administrador",
"label.all": "Todos",
"label.all-events": "Todos los eventos",
"label.all-time": "All time",
"label.all-websites": "Todos los sitios",
"label.back": "Atrás",
"label.cancel": "Cancelar",
@ -35,6 +36,7 @@
"label.more": "Más",
"label.name": "Nombre",
"label.new-password": "Nueva contraseña",
"label.owner": "Owner",
"label.password": "Contraseña",
"label.passwords-dont-match": "Las contraseñas no coinciden",
"label.profile": "Perfil",
@ -95,6 +97,7 @@
"metrics.filter.combined": "Combinado",
"metrics.filter.domain-only": "Únicamente dominio",
"metrics.filter.raw": "Personalizado",
"metrics.languages": "Languages",
"metrics.operating-systems": "Sistemas operativos",
"metrics.page-views": "Vistas",
"metrics.pages": "Páginas",

View File

@ -5,6 +5,7 @@
"label.administrator": "مدیر",
"label.all": "همه",
"label.all-events": "همه‌ی رویدادها",
"label.all-time": "All time",
"label.all-websites": "همه‌ی وب‌سایت‌ها",
"label.back": "برگشت",
"label.cancel": "انصراف",
@ -35,6 +36,7 @@
"label.more": "بیشتر",
"label.name": "نام",
"label.new-password": "رمز جدید",
"label.owner": "Owner",
"label.password": "رمز",
"label.passwords-dont-match": "رمزها یکسان نیستند",
"label.profile": "پروفایل",
@ -95,6 +97,7 @@
"metrics.filter.combined": "ترکیب شده",
"metrics.filter.domain-only": "فقط دامنه",
"metrics.filter.raw": "خام",
"metrics.languages": "Languages",
"metrics.operating-systems": "سیستم‌عامل‌ها",
"metrics.page-views": "بازدید صفحه",
"metrics.pages": "صفحه‌ها",

View File

@ -5,6 +5,7 @@
"label.administrator": "Järjestelmänvalvoja",
"label.all": "Kaikki",
"label.all-events": "All events",
"label.all-time": "All time",
"label.all-websites": "Kaikki verkkosivut",
"label.back": "Takaisin",
"label.cancel": "Peruuta",
@ -35,6 +36,7 @@
"label.more": "Lisää",
"label.name": "Nimi",
"label.new-password": "Uusi salasana",
"label.owner": "Owner",
"label.password": "Salasana",
"label.passwords-dont-match": "Salasanat eivät täsmää",
"label.profile": "Profiili",
@ -95,6 +97,7 @@
"metrics.filter.combined": "Yhdistetty",
"metrics.filter.domain-only": "Vain verkkotunnus",
"metrics.filter.raw": "Käsittelemätön",
"metrics.languages": "Languages",
"metrics.operating-systems": "Käyttöjärjestelmät",
"metrics.page-views": "Sivun näyttökertoja",
"metrics.pages": "Sivut",

View File

@ -5,6 +5,7 @@
"label.administrator": "Fyrisitari",
"label.all": "Alt",
"label.all-events": "All events",
"label.all-time": "All time",
"label.all-websites": "Allar heimasíður",
"label.back": "Aftur",
"label.cancel": "Strika",
@ -35,6 +36,7 @@
"label.more": "Meira",
"label.name": "Navn",
"label.new-password": "Nýtt loyniorð",
"label.owner": "Owner",
"label.password": "Loyniorð",
"label.passwords-dont-match": "Loyniorðini eru ikki eins",
"label.profile": "Vangi",
@ -95,6 +97,7 @@
"metrics.filter.combined": "Samansett",
"metrics.filter.domain-only": "Bara økisnavn",
"metrics.filter.raw": "Óviðgjørt",
"metrics.languages": "Languages",
"metrics.operating-systems": "Stýrikervir",
"metrics.page-views": "Opnaðar síðir",
"metrics.pages": "Síðir",

View File

@ -5,6 +5,7 @@
"label.administrator": "Administrateur",
"label.all": "Tout",
"label.all-events": "Tous les événements",
"label.all-time": "All time",
"label.all-websites": "Tous les sites web",
"label.back": "Retour",
"label.cancel": "Annuler",
@ -35,6 +36,7 @@
"label.more": "Plus",
"label.name": "Nom",
"label.new-password": "Nouveau mot de passe",
"label.owner": "Owner",
"label.password": "Mot de passe",
"label.passwords-dont-match": "Les mots de passe ne correspondent pas",
"label.profile": "Profil",
@ -95,6 +97,7 @@
"metrics.filter.combined": "Combiné",
"metrics.filter.domain-only": "Domaine uniquement",
"metrics.filter.raw": "Brute",
"metrics.languages": "Languages",
"metrics.operating-systems": "Systèmes d'exploitation",
"metrics.page-views": "Pages vues",
"metrics.pages": "Pages",

View File

@ -5,6 +5,7 @@
"label.administrator": "מנהל",
"label.all": "הכל",
"label.all-events": "All events",
"label.all-time": "All time",
"label.all-websites": "כל האתרים",
"label.back": "חזרה",
"label.cancel": "ביטול",
@ -35,6 +36,7 @@
"label.more": "עוד",
"label.name": "שם",
"label.new-password": "סיסמה חדשה",
"label.owner": "Owner",
"label.password": "סיסמה",
"label.passwords-dont-match": "סיסמאות לא תואמות",
"label.profile": "פרופיל",
@ -95,6 +97,7 @@
"metrics.filter.combined": "משותף",
"metrics.filter.domain-only": "דומיין בלבד",
"metrics.filter.raw": "גולמי",
"metrics.languages": "Languages",
"metrics.operating-systems": "מערכות הפעלה",
"metrics.page-views": "צפיות בדפים",
"metrics.pages": "דפים",

View File

@ -5,6 +5,7 @@
"label.administrator": "प्रशासक",
"label.all": "सब",
"label.all-events": "All events",
"label.all-time": "All time",
"label.all-websites": "सभी वेबसाइटें",
"label.back": "पीछे",
"label.cancel": "रद्द करें",
@ -35,6 +36,7 @@
"label.more": "और",
"label.name": "नाम",
"label.new-password": "नया पासवर्ड",
"label.owner": "Owner",
"label.password": "पासवर्ड",
"label.passwords-dont-match": "पासवर्ड मेल नहीं खाते",
"label.profile": "प्रोफ़ाइल",
@ -95,6 +97,7 @@
"metrics.filter.combined": "संयुक्त",
"metrics.filter.domain-only": "केवल डोमेन",
"metrics.filter.raw": "रॉ",
"metrics.languages": "Languages",
"metrics.operating-systems": "ऑपरेटिंग सिस्टम",
"metrics.page-views": "पृष्ठ दृश्य",
"metrics.pages": "पृष्ठों",

View File

@ -5,6 +5,7 @@
"label.administrator": "Adminisztrátor",
"label.all": "Összes",
"label.all-events": "Összes esemény",
"label.all-time": "All time",
"label.all-websites": "Összes weboldal",
"label.back": "Vissza",
"label.cancel": "Mégsem",
@ -35,6 +36,7 @@
"label.more": "Bővebben",
"label.name": "Név",
"label.new-password": "Új jelszó",
"label.owner": "Owner",
"label.password": "Jelszó",
"label.passwords-dont-match": "A jelszavak nem egyeznek",
"label.profile": "Profil",
@ -95,6 +97,7 @@
"metrics.filter.combined": "Összevont",
"metrics.filter.domain-only": "Csak domain",
"metrics.filter.raw": "Nyers",
"metrics.languages": "Languages",
"metrics.operating-systems": "Operációs rendszerek",
"metrics.page-views": "Oldalmegtekintések",
"metrics.pages": "Oldalak",

View File

@ -5,6 +5,7 @@
"label.administrator": "Pengelola",
"label.all": "Semua",
"label.all-events": "Semua peristiwa",
"label.all-time": "All time",
"label.all-websites": "Semua website",
"label.back": "Kembali",
"label.cancel": "Batal",
@ -35,6 +36,7 @@
"label.more": "Lebih banyak",
"label.name": "Nama",
"label.new-password": "Kata sandi baru",
"label.owner": "Owner",
"label.password": "Kata sandi",
"label.passwords-dont-match": "Kata sandi tidak cocok",
"label.profile": "Profil",
@ -95,6 +97,7 @@
"metrics.filter.combined": "Gabungan",
"metrics.filter.domain-only": "Hanya domain",
"metrics.filter.raw": "Mentah",
"metrics.languages": "Languages",
"metrics.operating-systems": "Sistem Operasi",
"metrics.page-views": "Tampilan halaman",
"metrics.pages": "Halaman",

View File

@ -5,6 +5,7 @@
"label.administrator": "Amministratore",
"label.all": "Tutto",
"label.all-events": "All events",
"label.all-time": "All time",
"label.all-websites": "Tutti i siti web",
"label.back": "Indietro",
"label.cancel": "Annulla",
@ -35,6 +36,7 @@
"label.more": "Dettagli",
"label.name": "Nome",
"label.new-password": "Nuova password",
"label.owner": "Owner",
"label.password": "Password",
"label.passwords-dont-match": "Le password non corrispondono",
"label.profile": "Profilo",
@ -95,6 +97,7 @@
"metrics.filter.combined": "Aggregati",
"metrics.filter.domain-only": "Solo dominio",
"metrics.filter.raw": "Raw",
"metrics.languages": "Languages",
"metrics.operating-systems": "Sistemi operativi",
"metrics.page-views": "Visualizzazioni di pagina",
"metrics.pages": "Pagine",

View File

@ -5,6 +5,7 @@
"label.administrator": "管理者",
"label.all": "すべて表示",
"label.all-events": "All events",
"label.all-time": "All time",
"label.all-websites": "すべてのWebサイト",
"label.back": "戻る",
"label.cancel": "キャンセル",
@ -35,6 +36,7 @@
"label.more": "さらに表示",
"label.name": "名前",
"label.new-password": "新しいパスワード",
"label.owner": "Owner",
"label.password": "パスワード",
"label.passwords-dont-match": "パスワードが一致しません",
"label.profile": "プロファイル",
@ -95,6 +97,7 @@
"metrics.filter.combined": "パスまで",
"metrics.filter.domain-only": "ドメインのみ",
"metrics.filter.raw": "すべて表示",
"metrics.languages": "Languages",
"metrics.operating-systems": "OS",
"metrics.page-views": "閲覧数",
"metrics.pages": "ページ",

View File

@ -5,6 +5,7 @@
"label.administrator": "관리자",
"label.all": "전체",
"label.all-events": "모든 이벤트",
"label.all-time": "All time",
"label.all-websites": "모든 웹사이트",
"label.back": "뒤로",
"label.cancel": "취소",
@ -35,6 +36,7 @@
"label.more": "더 보기",
"label.name": "이름",
"label.new-password": "새 비밀번호",
"label.owner": "Owner",
"label.password": "비밀번호",
"label.passwords-dont-match": "비밀번호가 일치하지 않음",
"label.profile": "프로필",
@ -95,6 +97,7 @@
"metrics.filter.combined": "합쳐서 보기",
"metrics.filter.domain-only": "도메인만",
"metrics.filter.raw": "전체 보기",
"metrics.languages": "Languages",
"metrics.operating-systems": "운영체제",
"metrics.page-views": "페이지 뷰(PV)",
"metrics.pages": "페이지",

View File

@ -5,6 +5,7 @@
"label.administrator": "Админ",
"label.all": "Бүх",
"label.all-events": "Бүх үйл явдал",
"label.all-time": "All time",
"label.all-websites": "Бүх вебүүд",
"label.back": "Буцах",
"label.cancel": "Цуцлах",
@ -35,6 +36,7 @@
"label.more": "Цааш",
"label.name": "Нэр",
"label.new-password": "Шинэ нууц үг",
"label.owner": "Owner",
"label.password": "Нууц үг",
"label.passwords-dont-match": "Нууц үг тохирохгүй байна",
"label.profile": "Бүртгэл",
@ -95,6 +97,7 @@
"metrics.filter.combined": "Нэгтгэсэн",
"metrics.filter.domain-only": "Зөвхөн домэйн",
"metrics.filter.raw": "Түүхий",
"metrics.languages": "Languages",
"metrics.operating-systems": "Үйлдлийн систем",
"metrics.page-views": "Хуудас үзсэн",
"metrics.pages": "Хуудас",

View File

@ -5,6 +5,7 @@
"label.administrator": "Pentadbir",
"label.all": "Semua",
"label.all-events": "Semua peristiwa",
"label.all-time": "All time",
"label.all-websites": "Semua laman web",
"label.back": "Kembali",
"label.cancel": "Batal",
@ -35,6 +36,7 @@
"label.more": "Lebih banyak lagi",
"label.name": "Nama",
"label.new-password": "Kata laluan baru",
"label.owner": "Owner",
"label.password": "Kata laluan",
"label.passwords-dont-match": "Kata laluan tidak sepadan",
"label.profile": "Profil",
@ -95,6 +97,7 @@
"metrics.filter.combined": "Digabungkan",
"metrics.filter.domain-only": "Domain sahaja",
"metrics.filter.raw": "Mentah",
"metrics.languages": "Languages",
"metrics.operating-systems": "Sistem operasi",
"metrics.page-views": "Paparan halaman",
"metrics.pages": "Halaman",

View File

@ -5,6 +5,7 @@
"label.administrator": "Administrator",
"label.all": "Alle",
"label.all-events": "All events",
"label.all-time": "All time",
"label.all-websites": "Alle nettsteder",
"label.back": "Tilbake",
"label.cancel": "Avvis",
@ -35,6 +36,7 @@
"label.more": "Mer",
"label.name": "Navn",
"label.new-password": "Nytt passord",
"label.owner": "Owner",
"label.password": "Passord",
"label.passwords-dont-match": "Passordene er ikke like",
"label.profile": "Profil",
@ -95,6 +97,7 @@
"metrics.filter.combined": "Kombinert",
"metrics.filter.domain-only": "Bare domene",
"metrics.filter.raw": "Rå",
"metrics.languages": "Languages",
"metrics.operating-systems": "Operativsystemer",
"metrics.page-views": "Sidevisninger",
"metrics.pages": "Sider",

View File

@ -5,6 +5,7 @@
"label.administrator": "Administrator",
"label.all": "Alles",
"label.all-events": "Alle gebeurtenissen",
"label.all-time": "All time",
"label.all-websites": "Alle websites",
"label.back": "Terug",
"label.cancel": "Annuleren",
@ -35,6 +36,7 @@
"label.more": "Toon meer",
"label.name": "Naam",
"label.new-password": "Nieuw wachtwoord",
"label.owner": "Owner",
"label.password": "Wachtwoord",
"label.passwords-dont-match": "Wachtwoorden komen niet overeen",
"label.profile": "Profiel",
@ -95,6 +97,7 @@
"metrics.filter.combined": "Gecombineerd",
"metrics.filter.domain-only": "Alleen domein",
"metrics.filter.raw": "Ruw",
"metrics.languages": "Languages",
"metrics.operating-systems": "Besturingssysteem",
"metrics.page-views": "Paginaweergaven",
"metrics.pages": "Pagina's",

View File

@ -5,6 +5,7 @@
"label.administrator": "Administrator",
"label.all": "Wszystkie",
"label.all-events": "Wszystkie wydarzenia",
"label.all-time": "All time",
"label.all-websites": "Wszystkie witryny",
"label.back": "Powrót",
"label.cancel": "Anuluj",
@ -35,6 +36,7 @@
"label.more": "Więcej",
"label.name": "Nazwa",
"label.new-password": "Nowe hasło",
"label.owner": "Owner",
"label.password": "Hasło",
"label.passwords-dont-match": "Hasła się nie zgadzają",
"label.profile": "Profil",
@ -95,6 +97,7 @@
"metrics.filter.combined": "Połączone",
"metrics.filter.domain-only": "Tylko domena",
"metrics.filter.raw": "Surowe dane",
"metrics.languages": "Languages",
"metrics.operating-systems": "System operacyjny",
"metrics.page-views": "Wyświetlenia strony",
"metrics.pages": "Strony",

View File

@ -5,6 +5,7 @@
"label.administrator": "Administrador",
"label.all": "Todos",
"label.all-events": "All events",
"label.all-time": "All time",
"label.all-websites": "Todos os sites",
"label.back": "Voltar",
"label.cancel": "Cancelar",
@ -35,6 +36,7 @@
"label.more": "Mais",
"label.name": "Nome",
"label.new-password": "Nova senha",
"label.owner": "Owner",
"label.password": "Senha",
"label.passwords-dont-match": "As senhas não correspondem",
"label.profile": "Perfil",
@ -95,6 +97,7 @@
"metrics.filter.combined": "Combinado",
"metrics.filter.domain-only": "Apenas domínio",
"metrics.filter.raw": "Dados brutos",
"metrics.languages": "Languages",
"metrics.operating-systems": "Sistemas operacionais",
"metrics.page-views": "Visualizações de página",
"metrics.pages": "Páginas",

View File

@ -5,6 +5,7 @@
"label.administrator": "Administrador",
"label.all": "Todos",
"label.all-events": "All events",
"label.all-time": "All time",
"label.all-websites": "Todos os websites",
"label.back": "Voltar",
"label.cancel": "Cancelar",
@ -35,6 +36,7 @@
"label.more": "Mais",
"label.name": "Nome",
"label.new-password": "Nova palavra-passe",
"label.owner": "Owner",
"label.password": "Palavra-passe",
"label.passwords-dont-match": "Palavra-passes não correspondem",
"label.profile": "Perfil",
@ -95,6 +97,7 @@
"metrics.filter.combined": "Combinado",
"metrics.filter.domain-only": "Apenas domínio",
"metrics.filter.raw": "Dados brutos",
"metrics.languages": "Languages",
"metrics.operating-systems": "Sistemas operativos",
"metrics.page-views": "Visualizações da página",
"metrics.pages": "Páginas",

View File

@ -3,9 +3,10 @@
"label.add-account": "Adăugare cont",
"label.add-website": "Adăugare site web",
"label.administrator": "Administrator",
"label.all": "All",
"label.all-events": "All events",
"label.all-websites": "All websites",
"label.all": "Toate",
"label.all-events": "Toate evenimentele",
"label.all-time": "All time",
"label.all-websites": "Toate site-urile web",
"label.back": "Înapoi",
"label.cancel": "Anulează",
"label.change-password": "Schimbare parolă",
@ -24,17 +25,18 @@
"label.edit": "Editare",
"label.edit-account": "Editare cont",
"label.edit-website": "Editare site web",
"label.enable-share-url": "Activare adresa URL de distribuire",
"label.enable-share-url": "Activare adresă URL de distribuire",
"label.invalid": "Invalid",
"label.invalid-domain": "Invalid domain",
"label.invalid-domain": "Domeniu nu este valid",
"label.last-days": "Ultimele {x} zile",
"label.last-hours": "Ultimele {x} ore",
"label.logged-in-as": "Autentificat ca {username}",
"label.login": "Autentificare",
"label.logout": "Dezautentificare",
"label.logout": "Iesire din cont",
"label.more": "Mai mult",
"label.name": "Nume",
"label.new-password": "Parola nouă",
"label.owner": "Owner",
"label.password": "Parolă",
"label.passwords-dont-match": "Parolele nu se potrivesc",
"label.profile": "Profil",
@ -43,7 +45,7 @@
"label.refresh": "Reîmprospătare",
"label.required": "Obligatoriu",
"label.reset": "Resetează",
"label.reset-website": "Reset statistics",
"label.reset-website": "Resetează statisticile pentru site",
"label.save": "Salvează",
"label.settings": "Setări",
"label.share-url": "Partajare URL",
@ -55,32 +57,32 @@
"label.today": "Astăzi",
"label.tracking-code": "Cod de urmărire",
"label.unknown": "Necunoscut",
"label.username": "Username",
"label.username": "Nume utilizator",
"label.view-details": "Vizualizare detalii",
"label.websites": "Site-uri web",
"message.active-users": "{x} {x, plural, one {vizitator activ} other {vizitatori activi}}",
"message.confirm-delete": "Sunteți sigur că doriți să ștergeți {target}?",
"message.confirm-reset": "Are your sure you want to reset {target}'s statistics?",
"message.confirm-reset": "Sunteți sigur că doriți să resetați statisticile pentru {target}?",
"message.copied": "Copiat!",
"message.delete-warning": "Toate datele asociate vor fi șterse, de asemenea.",
"message.failure": "Ceva n-a mers bine.",
"message.get-share-url": "Obține adresa URL de partajare",
"message.get-tracking-code": "Obține codul de urmărire",
"message.go-to-settings": "Mergi la Setări",
"message.incorrect-username-password": "Username/parolă incorecte.",
"message.log.visitor": "Visitor from {country} using {browser} on {os} {device}",
"message.incorrect-username-password": "Nume utilizator / parolă incorecte.",
"message.log.visitor": "Vizitator din {country} folosind {browser} pe {os} {device}",
"message.new-version-available": "Este disponibilă o nouă versiune {version} de umami!",
"message.no-data-available": "Nicio informație disponibilă.",
"message.no-data-available": "Nici o informație disponibilă.",
"message.no-websites-configured": "Nu aveți niciun site web configurat.",
"message.page-not-found": "Pagina nu a fost găsită.",
"message.powered-by": "Cu sprijinul {name}",
"message.reset-warning": "All statistics for this website will be deleted, but your tracking code will remain intact.",
"message.reset-warning": "Toate statisticile pentru acest site web vor fi șterse, dar codul de urmărire va rămâne intact.",
"message.save-success": "Salvat cu succes.",
"message.share-url": "Aceasta este adresa URL de partajare pentru {target}.",
"message.toggle-charts": "Toggle charts",
"message.toggle-charts": "Schimbă graficele",
"message.track-stats": "Pentru a urmări statisticile pentru {target}, plasați următorul cod în secțiunea {head} a site-ului dvs. web.",
"message.type-delete": "Tastați {delete} în casuța de mai jos pentru a confirma.",
"message.type-reset": "Type {reset} in the box below to confirm.",
"message.type-reset": "Introduceți {reset} în căsuța de mai jos pentru a confirma.",
"metrics.actions": "Acțiuni",
"metrics.average-visit-time": "Timp mediu de vizitare",
"metrics.bounce-rate": "Rata de respingere",
@ -95,6 +97,7 @@
"metrics.filter.combined": "Combinat",
"metrics.filter.domain-only": "Numai domeniu",
"metrics.filter.raw": "Brut",
"metrics.languages": "Languages",
"metrics.operating-systems": "Sisteme de operare",
"metrics.page-views": "Vizualizări de pagină",
"metrics.pages": "Pagini",

View File

@ -5,6 +5,7 @@
"label.administrator": "Администратор",
"label.all": "Все",
"label.all-events": "Все события",
"label.all-time": "All time",
"label.all-websites": "Все сайты",
"label.back": "Назад",
"label.cancel": "Отменить",
@ -35,6 +36,7 @@
"label.more": "Больше",
"label.name": "Имя",
"label.new-password": "Новый пароль",
"label.owner": "Owner",
"label.password": "Пароль",
"label.passwords-dont-match": "Пароли не совпадают",
"label.profile": "Профиль",
@ -95,6 +97,7 @@
"metrics.filter.combined": "Объединенные",
"metrics.filter.domain-only": "Только домен",
"metrics.filter.raw": "Сырые данные",
"metrics.languages": "Languages",
"metrics.operating-systems": "Операционные системы",
"metrics.page-views": "Просмотры страниц",
"metrics.pages": "Страницы",

View File

@ -5,6 +5,7 @@
"label.administrator": "Administrátor",
"label.all": "Všetko",
"label.all-events": "All events",
"label.all-time": "All time",
"label.all-websites": "Všetky weby",
"label.back": "Späť",
"label.cancel": "Zrušiť",
@ -35,6 +36,7 @@
"label.more": "Viac",
"label.name": "Meno",
"label.new-password": "Nové heslo",
"label.owner": "Owner",
"label.password": "Heslo",
"label.passwords-dont-match": "Hesla se nezhodujú",
"label.profile": "Profil",
@ -95,6 +97,7 @@
"metrics.filter.combined": "Kombinácie",
"metrics.filter.domain-only": "Domény",
"metrics.filter.raw": "Nezpracované",
"metrics.languages": "Languages",
"metrics.operating-systems": "Operačný systém",
"metrics.page-views": "Zobrazenie stánok",
"metrics.pages": "Stránky",

View File

@ -5,6 +5,7 @@
"label.administrator": "Administrator",
"label.all": "Vse",
"label.all-events": "Vsi dogodki",
"label.all-time": "All time",
"label.all-websites": "Vsa spletna mesta",
"label.back": "Nazaj",
"label.cancel": "Prekliči",
@ -35,6 +36,7 @@
"label.more": "Več",
"label.name": "Ime",
"label.new-password": "Novo geslo",
"label.owner": "Owner",
"label.password": "Geslo",
"label.passwords-dont-match": "Gesli se ne ujemata",
"label.profile": "Profil",
@ -95,6 +97,7 @@
"metrics.filter.combined": "Skupno",
"metrics.filter.domain-only": "Samo domena",
"metrics.filter.raw": "Neobdelane meritve",
"metrics.languages": "Languages",
"metrics.operating-systems": "Operacijski sistemi",
"metrics.page-views": "Ogledi strani",
"metrics.pages": "Strani",

View File

@ -5,6 +5,7 @@
"label.administrator": "Administratör",
"label.all": "Alla",
"label.all-events": "All events",
"label.all-time": "All time",
"label.all-websites": "Alla sajter",
"label.back": "Tillbaka",
"label.cancel": "Avbryt",
@ -35,6 +36,7 @@
"label.more": "Mer",
"label.name": "Namn",
"label.new-password": "Nytt lösenord",
"label.owner": "Owner",
"label.password": "Lösenord",
"label.passwords-dont-match": "Lösenorden är inte samma",
"label.profile": "Profil",
@ -95,6 +97,7 @@
"metrics.filter.combined": "Kombinerade",
"metrics.filter.domain-only": "Endast domän",
"metrics.filter.raw": "Rådata",
"metrics.languages": "Languages",
"metrics.operating-systems": "Operativsystem",
"metrics.page-views": "Sidvisningar",
"metrics.pages": "Sidor",

View File

@ -5,6 +5,7 @@
"label.administrator": "நிர்வாகியைச் சேர்க்க",
"label.all": "எல்லாம்",
"label.all-events": "அனைத்து நிகழ்வுகளும்",
"label.all-time": "All time",
"label.all-websites": "அனைத்து வலைத்தளங்களும்",
"label.back": "பின்னால்",
"label.cancel": "ரத்துசெய்",
@ -35,6 +36,7 @@
"label.more": "மேலும்",
"label.name": "பெயர்",
"label.new-password": "புதிய கடவுச்சொல்",
"label.owner": "Owner",
"label.password": "கடவுச்சொல்",
"label.passwords-dont-match": "இருக்கடவுச்சொல் பொருந்தவில்லை",
"label.profile": "சுயவிவரம்",
@ -95,6 +97,7 @@
"metrics.filter.combined": "ஒருங்கிணைந்த",
"metrics.filter.domain-only": "கள முகவரி மட்டும்",
"metrics.filter.raw": "மூல",
"metrics.languages": "Languages",
"metrics.operating-systems": "இயக்க முறைமைகள்",
"metrics.page-views": "பக்க காட்சிகள்",
"metrics.pages": "பக்கங்கள்",

View File

@ -5,6 +5,7 @@
"label.administrator": "Yönetici",
"label.all": "Tümü",
"label.all-events": "All events",
"label.all-time": "All time",
"label.all-websites": "Tüm web siteleri",
"label.back": "Geri",
"label.cancel": "İptal",
@ -35,6 +36,7 @@
"label.more": "Detaylı göster",
"label.name": "İsim",
"label.new-password": "Yeni parola",
"label.owner": "Owner",
"label.password": "Parola",
"label.passwords-dont-match": "Parolalar uyuşmuyor",
"label.profile": "Profil",
@ -95,6 +97,7 @@
"metrics.filter.combined": "Birleşik",
"metrics.filter.domain-only": "Yalnızca alan adı",
"metrics.filter.raw": "Ham",
"metrics.languages": "Languages",
"metrics.operating-systems": "İşletim sistemi",
"metrics.page-views": "Sayfa görünümü",
"metrics.pages": "Sayfalar",

View File

@ -5,6 +5,7 @@
"label.administrator": "Адміністратор",
"label.all": "Всі",
"label.all-events": "Всі події",
"label.all-time": "All time",
"label.all-websites": "Всі сайти",
"label.back": "Назад",
"label.cancel": "Відмінити",
@ -35,6 +36,7 @@
"label.more": "Більше",
"label.name": "Ім'я",
"label.new-password": "Новий пароль",
"label.owner": "Owner",
"label.password": "Пароль",
"label.passwords-dont-match": "Паролі не співпадають",
"label.profile": "Профіль",
@ -95,6 +97,7 @@
"metrics.filter.combined": "Об'єднані",
"metrics.filter.domain-only": "Лише домен",
"metrics.filter.raw": "Сирі дані",
"metrics.languages": "Languages",
"metrics.operating-systems": "Операційні системи",
"metrics.page-views": "Перегляди сторінок",
"metrics.pages": "Сторінки",

View File

@ -5,6 +5,7 @@
"label.administrator": "Quản Trị",
"label.all": "Tất cả",
"label.all-events": "Tất cả events",
"label.all-time": "All time",
"label.all-websites": "Tất cả websites",
"label.back": "Quay về",
"label.cancel": "Huỷ bỏ",
@ -35,6 +36,7 @@
"label.more": "Thêm",
"label.name": "Tên",
"label.new-password": "Mật khẩu mới",
"label.owner": "Owner",
"label.password": "Mật khẩu",
"label.passwords-dont-match": "Mật khẩu không đồng nhất",
"label.profile": "Hồ sơ",
@ -95,6 +97,7 @@
"metrics.filter.combined": "Kết hợp",
"metrics.filter.domain-only": "Chỉ tên miền",
"metrics.filter.raw": "Gốc",
"metrics.languages": "Languages",
"metrics.operating-systems": "Hệ điều hành",
"metrics.page-views": "Lượt xem",
"metrics.pages": "Trang",

View File

@ -5,6 +5,7 @@
"label.administrator": "管理员",
"label.all": "所有",
"label.all-events": "All events",
"label.all-time": "All time",
"label.all-websites": "全部网站",
"label.back": "返回",
"label.cancel": "取消",
@ -35,6 +36,7 @@
"label.more": "更多",
"label.name": "名字",
"label.new-password": "新密码",
"label.owner": "Owner",
"label.password": "密码",
"label.passwords-dont-match": "密码不一致",
"label.profile": "个人资料",
@ -95,6 +97,7 @@
"metrics.filter.combined": "总和",
"metrics.filter.domain-only": "只看域名",
"metrics.filter.raw": "原始",
"metrics.languages": "Languages",
"metrics.operating-systems": "操作系统",
"metrics.page-views": "页面浏览量",
"metrics.pages": "网页",

View File

@ -5,6 +5,7 @@
"label.administrator": "管理員",
"label.all": "所有",
"label.all-events": "All events",
"label.all-time": "All time",
"label.all-websites": "全部網站",
"label.back": "返回",
"label.cancel": "取消",
@ -35,6 +36,7 @@
"label.more": "更多",
"label.name": "名字",
"label.new-password": "新密碼",
"label.owner": "Owner",
"label.password": "密碼",
"label.passwords-dont-match": "密碼不一致",
"label.profile": "個人資料",
@ -95,6 +97,7 @@
"metrics.filter.combined": "總和",
"metrics.filter.domain-only": "僅域名",
"metrics.filter.raw": "原始",
"metrics.languages": "Languages",
"metrics.operating-systems": "操作系统",
"metrics.page-views": "網頁流量",
"metrics.pages": "網頁",

View File

@ -39,7 +39,11 @@ export function getDateRange(value, locale = 'en-US') {
const now = new Date();
const dateLocale = getDateLocale(locale);
const { num, unit } = value.match(/^(?<num>[0-9]+)(?<unit>hour|day|week|month|year)$/).groups;
const match = value.match(/^(?<num>[0-9]+)(?<unit>hour|day|week|month|year)$/);
if (!match) return;
const { num, unit } = match.groups;
if (+num === 1) {
switch (unit) {

View File

@ -36,7 +36,7 @@ export async function rawQuery(query, params = []) {
const sql = db === MYSQL ? query.replace(/\$[0-9]+/g, '?') : query;
return prisma.$queryRaw.apply(prisma, [sql, ...params]);
return prisma.$queryRawUnsafe.apply(prisma, [sql, ...params]);
}
export function getDateQuery(field, unit, timezone) {
@ -115,6 +115,29 @@ export async function getUserWebsites(user_id) {
);
}
export async function getAllWebsites() {
let data = await runQuery(
prisma.website.findMany({
orderBy: [
{
user_id: 'asc',
},
{
name: 'asc',
},
],
include: {
account: {
select: {
username: true,
},
},
},
}),
);
return data.map(i => ({ ...i, account: i.account.username }));
}
export async function createWebsite(user_id, data) {
return runQuery(
prisma.website.create({
@ -147,14 +170,11 @@ export async function resetWebsite(website_id) {
export async function deleteWebsite(website_id) {
return runQuery(
/* Prisma bug, does not cascade on non-nullable foreign keys
prisma.website.delete({
where: {
website_id,
},
}),
*/
prisma.$queryRaw`delete from website where website_id=${website_id}`,
);
}
@ -246,14 +266,11 @@ export async function updateAccount(user_id, data) {
export async function deleteAccount(user_id) {
return runQuery(
/* Prisma bug, does not cascade on non-nullable foreign keys
prisma.account.delete({
where: {
user_id,
},
}),
*/
prisma.$queryRaw`delete from account where user_id=${user_id}`,
);
}

View File

@ -37,16 +37,22 @@ export async function getSession(req) {
let session = await getSessionByUuid(session_uuid);
if (!session) {
session = await createSession(website_id, {
session_uuid,
hostname,
browser,
os,
screen,
language,
country,
device,
});
try {
session = await createSession(website_id, {
session_uuid,
hostname,
browser,
os,
screen,
language,
country,
device,
});
} catch (e) {
if (!e.message.includes('Unique constraint')) {
throw e;
}
}
}
const { session_id } = session;

View File

@ -1,6 +1,6 @@
{
"name": "umami",
"version": "1.24.1",
"version": "1.25.0",
"description": "A simple, fast, website analytics alternative to Google Analytics. ",
"author": "Mike Cao <mike@mikecao.com>",
"license": "MIT",
@ -33,6 +33,8 @@
"compile-lang": "formatjs compile-folder --ast build public/lang",
"check-lang": "node scripts/check-lang.js",
"download-country-names": "node scripts/download-country-names.js",
"download-language-names": "node scripts/download-language-names.js",
"change-password": "node scripts/change-password.js",
"loadtest": "node scripts/loadtest.js",
"loadtest:medium": "node scripts/loadtest.js --weight=medium",
"loadtest:heavy": "node scripts/loadtest.js --weight=heavy --verbose",
@ -54,12 +56,13 @@
},
"dependencies": {
"@fontsource/inter": "4.5.0",
"@prisma/client": "2.29.1",
"@prisma/client": "3.6.0",
"@reduxjs/toolkit": "^1.6.1",
"bcryptjs": "^2.4.3",
"chalk": "^4.1.1",
"chart.js": "^2.9.4",
"classnames": "^2.3.1",
"colord": "^2.9.2",
"cookie": "^0.4.1",
"cors": "^2.8.5",
"date-fns": "^2.23.0",
@ -74,7 +77,7 @@
"jose": "2.0.5",
"maxmind": "^4.3.2",
"moment-timezone": "^0.5.33",
"next": "12.0.1",
"next": "12.0.5",
"prompts": "2.4.2",
"prop-types": "^15.7.2",
"react": "^17.0.2",
@ -92,7 +95,6 @@
"semver": "^7.3.5",
"thenby": "^1.3.4",
"timezone-support": "^2.0.2",
"tinycolor2": "^1.4.2",
"uuid": "^8.3.2"
},
"devDependencies": {
@ -122,7 +124,7 @@
"postcss-rtlcss": "^3.3.2",
"prettier": "^2.3.2",
"prettier-eslint": "^13.0.0",
"prisma": "2.29.1",
"prisma": "3.6.0",
"rollup": "^2.48.0",
"rollup-plugin-hashbang": "^2.2.2",
"rollup-plugin-terser": "^7.0.2",

View File

@ -2,7 +2,7 @@ import { getPageviewMetrics, getSessionMetrics, getWebsiteById } from 'lib/queri
import { ok, methodNotAllowed, unauthorized, badRequest } from 'lib/response';
import { allowQuery } from 'lib/auth';
const sessionColumns = ['browser', 'os', 'device', 'country'];
const sessionColumns = ['browser', 'os', 'device', 'country', 'language'];
const pageviewColumns = ['url', 'referrer'];
function getTable(type) {
@ -37,7 +37,19 @@ export default async (req, res) => {
const endDate = new Date(+end_at);
if (sessionColumns.includes(type)) {
const data = await getSessionMetrics(websiteId, startDate, endDate, type, { url });
let data = await getSessionMetrics(websiteId, startDate, endDate, type, { url });
if (type === 'language') {
let combined = {};
for (let { x, y } of data) {
x = String(x).toLowerCase().split('-')[0];
if (!combined[x]) combined[x] = { x, y };
else combined[x].y += y;
}
data = Object.values(combined);
}
return ok(res, data);
}

View File

@ -1,4 +1,4 @@
import { getUserWebsites } from 'lib/queries';
import { getAllWebsites, getUserWebsites } from 'lib/queries';
import { useAuth } from 'lib/middleware';
import { ok, methodNotAllowed, unauthorized } from 'lib/response';
@ -6,7 +6,7 @@ export default async (req, res) => {
await useAuth(req, res);
const { user_id: current_user_id, is_admin } = req.auth;
const { user_id } = req.query;
const { user_id, include_all } = req.query;
const userId = +user_id;
if (req.method === 'GET') {
@ -14,7 +14,10 @@ export default async (req, res) => {
return unauthorized(res);
}
const websites = await getUserWebsites(userId || current_user_id);
const websites =
is_admin && include_all
? await getAllWebsites()
: await getUserWebsites(userId || current_user_id);
return ok(res, websites);
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

184
public/language/en-US.json Normal file
View File

@ -0,0 +1,184 @@
{
"ab": "Abkhaz",
"aa": "Afar",
"af": "Afrikaans",
"ak": "Akan",
"sq": "Albanian",
"am": "Amharic",
"ar": "Arabic",
"an": "Aragonese",
"hy": "Armenian",
"as": "Assamese",
"av": "Avaric",
"ae": "Avestan",
"ay": "Aymara",
"az": "Azerbaijani",
"bm": "Bambara",
"ba": "Bashkir",
"eu": "Basque",
"be": "Belarusian",
"bn": "Bengali",
"bh": "Bihari",
"bi": "Bislama",
"bs": "Bosnian",
"br": "Breton",
"bg": "Bulgarian",
"my": "Burmese",
"ca": "Catalan; Valencian",
"ch": "Chamorro",
"ce": "Chechen",
"ny": "Chichewa; Chewa; Nyanja",
"zh": "Chinese",
"cv": "Chuvash",
"kw": "Cornish",
"co": "Corsican",
"cr": "Cree",
"hr": "Croatian",
"cs": "Czech",
"da": "Danish",
"dv": "Divehi; Dhivehi; Maldivian;",
"nl": "Dutch",
"en": "English",
"eo": "Esperanto",
"et": "Estonian",
"ee": "Ewe",
"fo": "Faroese",
"fj": "Fijian",
"fi": "Finnish",
"fr": "French",
"ff": "Fula; Fulah; Pulaar; Pular",
"gl": "Galician",
"ka": "Georgian",
"de": "German",
"el": "Greek, Modern",
"gn": "Guaraní",
"gu": "Gujarati",
"ht": "Haitian; Haitian Creole",
"ha": "Hausa",
"he": "Hebrew (modern)",
"hz": "Herero",
"hi": "Hindi",
"ho": "Hiri Motu",
"hu": "Hungarian",
"ia": "Interlingua",
"id": "Indonesian",
"ie": "Interlingue",
"ga": "Irish",
"ig": "Igbo",
"ik": "Inupiaq",
"io": "Ido",
"is": "Icelandic",
"it": "Italian",
"iu": "Inuktitut",
"ja": "Japanese",
"jv": "Javanese",
"kl": "Kalaallisut, Greenlandic",
"kn": "Kannada",
"kr": "Kanuri",
"ks": "Kashmiri",
"kk": "Kazakh",
"km": "Khmer",
"ki": "Kikuyu, Gikuyu",
"rw": "Kinyarwanda",
"ky": "Kirghiz, Kyrgyz",
"kv": "Komi",
"kg": "Kongo",
"ko": "Korean",
"ku": "Kurdish",
"kj": "Kwanyama, Kuanyama",
"la": "Latin",
"lb": "Luxembourgish, Letzeburgesch",
"lg": "Luganda",
"li": "Limburgish, Limburgan, Limburger",
"ln": "Lingala",
"lo": "Lao",
"lt": "Lithuanian",
"lu": "Luba-Katanga",
"lv": "Latvian",
"gv": "Manx",
"mk": "Macedonian",
"mg": "Malagasy",
"ms": "Malay",
"ml": "Malayalam",
"mt": "Maltese",
"mi": "Māori",
"mr": "Marathi (Marāṭhī)",
"mh": "Marshallese",
"mn": "Mongolian",
"na": "Nauru",
"nv": "Navajo, Navaho",
"nb": "Norwegian Bokmål",
"nd": "North Ndebele",
"ne": "Nepali",
"ng": "Ndonga",
"nn": "Norwegian Nynorsk",
"no": "Norwegian",
"ii": "Nuosu",
"nr": "South Ndebele",
"oc": "Occitan",
"oj": "Ojibwe, Ojibwa",
"cu": "Old Church Slavonic, Church Slavic, Church Slavonic, Old Bulgarian, Old Slavonic",
"om": "Oromo",
"or": "Oriya",
"os": "Ossetian, Ossetic",
"pa": "Panjabi, Punjabi",
"pi": "Pāli",
"fa": "Persian",
"pl": "Polish",
"ps": "Pashto, Pushto",
"pt": "Portuguese",
"qu": "Quechua",
"rm": "Romansh",
"rn": "Kirundi",
"ro": "Romanian, Moldavian, Moldovan",
"ru": "Russian",
"sa": "Sanskrit (Saṁskṛta)",
"sc": "Sardinian",
"sd": "Sindhi",
"se": "Northern Sami",
"sm": "Samoan",
"sg": "Sango",
"sr": "Serbian",
"gd": "Scottish Gaelic; Gaelic",
"sn": "Shona",
"si": "Sinhala, Sinhalese",
"sk": "Slovak",
"sl": "Slovene",
"so": "Somali",
"st": "Southern Sotho",
"es": "Spanish; Castilian",
"su": "Sundanese",
"sw": "Swahili",
"ss": "Swati",
"sv": "Swedish",
"ta": "Tamil",
"te": "Telugu",
"tg": "Tajik",
"th": "Thai",
"ti": "Tigrinya",
"bo": "Tibetan Standard, Tibetan, Central",
"tk": "Turkmen",
"tl": "Tagalog",
"tn": "Tswana",
"to": "Tonga (Tonga Islands)",
"tr": "Turkish",
"ts": "Tsonga",
"tt": "Tatar",
"tw": "Twi",
"ty": "Tahitian",
"ug": "Uighur, Uyghur",
"uk": "Ukrainian",
"ur": "Urdu",
"uz": "Uzbek",
"ve": "Venda",
"vi": "Vietnamese",
"vo": "Volapük",
"wa": "Walloon",
"cy": "Welsh",
"wo": "Wolof",
"fy": "Western Frisian",
"xh": "Xhosa",
"yi": "Yiddish",
"yo": "Yoruba",
"za": "Zhuang, Chuang"
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

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