Merge pull request #762 from mikecao/dev

v1.22.0
This commit is contained in:
Mike Cao 2021-08-20 01:15:43 -07:00 committed by GitHub
commit 9b1a75fd90
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 914 additions and 723 deletions

View File

@ -0,0 +1,98 @@
import React, { useState } from 'react';
import { FormattedMessage } from 'react-intl';
import { Formik, Form, Field } from 'formik';
import Button from 'components/common/Button';
import FormLayout, {
FormButtons,
FormError,
FormMessage,
FormRow,
} from 'components/layout/FormLayout';
import usePost from 'hooks/usePost';
const CONFIRMATION_WORD = 'RESET';
const validate = ({ confirmation }) => {
const errors = {};
if (confirmation !== CONFIRMATION_WORD) {
errors.confirmation = !confirmation ? (
<FormattedMessage id="label.required" defaultMessage="Required" />
) : (
<FormattedMessage id="label.invalid" defaultMessage="Invalid" />
);
}
return errors;
};
export default function ResetForm({ values, onSave, onClose }) {
const post = usePost();
const [message, setMessage] = useState();
const handleSubmit = async ({ type, id }) => {
const { ok, data } = await post(`/api/${type}/${id}/reset`);
if (ok) {
onSave();
} else {
setMessage(
data || <FormattedMessage id="message.failure" defaultMessage="Something went wrong." />,
);
}
};
return (
<FormLayout>
<Formik
initialValues={{ confirmation: '', ...values }}
validate={validate}
onSubmit={handleSubmit}
>
{props => (
<Form>
<div>
<FormattedMessage
id="message.confirm-reset"
defaultMessage="Are your sure you want to reset {target}'s statistics?"
values={{ target: <b>{values.name}</b> }}
/>
</div>
<div>
<FormattedMessage
id="message.reset-warning"
defaultMessage="All statistics for this website will be deleted, but your tracking code will remain intact."
/>
</div>
<p>
<FormattedMessage
id="message.type-reset"
defaultMessage="Type {reset} in the box below to confirm."
values={{ reset: <b>{CONFIRMATION_WORD}</b> }}
/>
</p>
<FormRow>
<div>
<Field name="confirmation" type="text" />
<FormError name="confirmation" />
</div>
</FormRow>
<FormButtons>
<Button
type="submit"
variant="danger"
disabled={props.values.confirmation !== CONFIRMATION_WORD}
>
<FormattedMessage id="label.reset" defaultMessage="Reset" />
</Button>
<Button onClick={onClose}>
<FormattedMessage id="label.cancel" defaultMessage="Cancel" />
</Button>
</FormButtons>
<FormMessage>{message}</FormMessage>
</Form>
)}
</Formik>
</FormLayout>
);
}

View File

@ -3,13 +3,38 @@ import { useSpring, animated } from 'react-spring';
import { formatNumber } from '../../lib/format';
import styles from './MetricCard.module.css';
const MetricCard = ({ value = 0, label, format = formatNumber }) => {
const MetricCard = ({
value = 0,
change = 0,
label,
reverseColors = false,
format = formatNumber,
}) => {
const props = useSpring({ x: Number(value) || 0, from: { x: 0 } });
const changeProps = useSpring({ x: Number(change) || 0, from: { x: 0 } });
return (
<div className={styles.card}>
<animated.div className={styles.value}>{props.x.interpolate(x => format(x))}</animated.div>
<div className={styles.label}>{label}</div>
<div className={styles.label}>
{label}
{~~change === 0 && <span className={styles.change}>{format(0)}</span>}
{~~change !== 0 && (
<animated.span
className={`${styles.change} ${
change >= 0
? !reverseColors
? styles.positive
: styles.negative
: !reverseColors
? styles.negative
: styles.positive
}`}
>
{changeProps.x.interpolate(x => `${change >= 0 ? '+' : ''}${format(x)}`)}
</animated.span>
)}
</div>
</div>
);
};

View File

@ -16,4 +16,24 @@
.label {
font-size: var(--font-size-normal);
white-space: nowrap;
display: flex;
align-items: center;
gap: 5px;
}
.change {
font-size: 12px;
padding: 0 5px;
border-radius: 5px;
margin-left: 4px;
border: 1px solid var(--gray200);
color: var(--gray500);
}
.change.positive {
color: var(--green500);
}
.change.negative {
color: var(--red500);
}

View File

@ -34,14 +34,22 @@ export default function MetricsBar({ websiteId, className }) {
[url, modified],
);
const formatFunc = format ? formatLongNumber : formatNumber;
const formatFunc = format
? n => (n >= 0 ? formatLongNumber(n) : `-${formatLongNumber(Math.abs(n))}`)
: formatNumber;
function handleSetFormat() {
setFormat(state => !state);
}
const { pageviews, uniques, bounces, totaltime } = data || {};
const num = Math.min(uniques, bounces);
const num = Math.min(data && uniques.value, data && bounces.value);
const diffs = data && {
pageviews: pageviews.value - pageviews.change,
uniques: uniques.value - uniques.change,
bounces: bounces.value - bounces.change,
totaltime: totaltime.value - totaltime.change,
};
return (
<div className={classNames(styles.bar, className)} onClick={handleSetFormat}>
@ -51,18 +59,27 @@ export default function MetricsBar({ websiteId, className }) {
<>
<MetricCard
label={<FormattedMessage id="metrics.views" defaultMessage="Views" />}
value={pageviews}
value={pageviews.value}
change={pageviews.change}
format={formatFunc}
/>
<MetricCard
label={<FormattedMessage id="metrics.visitors" defaultMessage="Visitors" />}
value={uniques}
value={uniques.value}
change={uniques.change}
format={formatFunc}
/>
<MetricCard
label={<FormattedMessage id="metrics.bounce-rate" defaultMessage="Bounce rate" />}
value={uniques ? (num / uniques) * 100 : 0}
value={uniques.value ? (num / uniques.value) * 100 : 0}
change={
uniques.value && uniques.change
? (num / uniques.value) * 100 -
(Math.min(diffs.uniques, diffs.bounces) / diffs.uniques) * 100 || 0
: 0
}
format={n => Number(n).toFixed(0) + '%'}
reverseColors
/>
<MetricCard
label={
@ -71,8 +88,19 @@ export default function MetricsBar({ websiteId, className }) {
defaultMessage="Average visit time"
/>
}
value={totaltime && pageviews ? totaltime / (pageviews - bounces) : 0}
format={n => formatShortTime(n, ['m', 's'], ' ')}
value={
totaltime.value && pageviews.value
? totaltime.value / (pageviews.value - bounces.value)
: 0
}
change={
totaltime.value && pageviews.value
? (diffs.totaltime / (diffs.pageviews - diffs.bounces) -
totaltime.value / (pageviews.value - bounces.value)) *
-1 || 0
: 0
}
format={n => `${n < 0 ? '-' : ''}${formatShortTime(Math.abs(~~n), ['m', 's'], ' ')}`}
/>
</>
)}

View File

@ -7,6 +7,7 @@ import Button from 'components/common/Button';
import PageHeader from 'components/layout/PageHeader';
import Modal from 'components/common/Modal';
import WebsiteEditForm from 'components/forms/WebsiteEditForm';
import ResetForm from 'components/forms/ResetForm';
import DeleteForm from 'components/forms/DeleteForm';
import TrackingCodeForm from 'components/forms/TrackingCodeForm';
import ShareUrlForm from 'components/forms/ShareUrlForm';
@ -16,6 +17,7 @@ import Toast from 'components/common/Toast';
import Favicon from 'components/common/Favicon';
import Pen from 'assets/pen.svg';
import Trash from 'assets/trash.svg';
import Reset from 'assets/redo.svg';
import Plus from 'assets/plus.svg';
import Code from 'assets/code.svg';
import LinkIcon from 'assets/link.svg';
@ -24,6 +26,7 @@ import styles from './WebsiteSettings.module.css';
export default function WebsiteSettings() {
const [editWebsite, setEditWebsite] = useState();
const [resetWebsite, setResetWebsite] = useState();
const [deleteWebsite, setDeleteWebsite] = useState();
const [addWebsite, setAddWebsite] = useState();
const [showCode, setShowCode] = useState();
@ -55,6 +58,9 @@ export default function WebsiteSettings() {
<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>
@ -96,6 +102,7 @@ export default function WebsiteSettings() {
function handleClose() {
setAddWebsite(null);
setEditWebsite(null);
setResetWebsite(null);
setDeleteWebsite(null);
setShowCode(null);
setShowUrl(null);
@ -141,6 +148,17 @@ export default function WebsiteSettings() {
<WebsiteEditForm onSave={handleSave} onClose={handleClose} />
</Modal>
)}
{resetWebsite && (
<Modal
title={<FormattedMessage id="label.reset-website" defaultMessage="Reset statistics" />}
>
<ResetForm
values={{ type: 'website', id: resetWebsite.website_id, name: resetWebsite.name }}
onSave={handleSave}
onClose={handleClose}
/>
</Modal>
)}
{deleteWebsite && (
<Modal
title={<FormattedMessage id="label.delete-website" defaultMessage="Delete website" />}

View File

@ -19,6 +19,7 @@
"label.delete": "Delete",
"label.delete-account": "Delete account",
"label.delete-website": "Delete website",
"label.reset-website": "Reset statistics",
"label.dismiss": "Dismiss",
"label.domain": "Domain",
"label.edit": "Edit",
@ -58,8 +59,10 @@
"label.view-details": "View details",
"label.websites": "Websites",
"message.active-users": "{x} current {x, plural, one {visitor} other {visitors}}",
"message.confirm-reset": "Are your sure you want to reset {target}'s statistics?",
"message.confirm-delete": "Are your sure you want to delete {target}?",
"message.copied": "Copied!",
"message.reset-warning": "All statistics for this website will be deleted, but your tracking code will remain intact.",
"message.delete-warning": "All associated data will be deleted as well.",
"message.failure": "Something went wrong.",
"message.get-share-url": "Get share URL",

View File

@ -5,6 +5,7 @@
"label.administrator": "Administrator",
"label.all": "Wszystkie",
"label.all-websites": "Wszystkie witryny",
"label.all-events": "Wszystkie wydarzenia",
"label.back": "Powrót",
"label.cancel": "Anuluj",
"label.change-password": "Zmień hasło",

100
lang/sl-SI.json Normal file
View File

@ -0,0 +1,100 @@
{
"label.accounts": "Računi",
"label.add-account": "Dodaj račun",
"label.add-website": "Dodaj spletno mesto",
"label.administrator": "Administrator",
"label.all": "Vse",
"label.all-websites": "Vsa spletna mesta",
"label.all-events": "Vsi dogodki",
"label.back": "Nazaj",
"label.cancel": "Prekliči",
"label.change-password": "Zamenjaj geslo",
"label.confirm-password": "Potrditev gesla",
"label.copy-to-clipboard": "Kopiraj v odložišče",
"label.current-password": "Trenutno geslo",
"label.custom-range": "Razpon po meri",
"label.dashboard": "Nadzorna plošča",
"label.date-range": "Časovni razpon",
"label.default-date-range": "Privzeti časovni razpon",
"label.delete": "Izbriši",
"label.delete-account": "Izbriši račun",
"label.delete-website": "Izbriši spletno mesto",
"label.dismiss": "Opusti",
"label.domain": "Domena",
"label.edit": "Uredi",
"label.edit-account": "Uredi račun",
"label.edit-website": "Uredi spletno stran",
"label.enable-share-url": "Omogoči URL za skupno rabo",
"label.invalid": "Neveljavno",
"label.invalid-domain": "Neveljavna domena",
"label.last-days": "Zadnjih {x} dni",
"label.last-hours": "Zadnjih {x} ur",
"label.logged-in-as": "Prijavljen kot {username}",
"label.login": "Prijava",
"label.logout": "Odjava",
"label.more": "Več",
"label.name": "Ime",
"label.new-password": "Novo geslo",
"label.password": "Geslo",
"label.passwords-dont-match": "Gesli se ne ujemata",
"label.profile": "Profil",
"label.realtime": "V realnem času",
"label.realtime-logs": "Dnevnik v realnem času",
"label.refresh": "Osveži",
"label.required": "Zahtevano",
"label.reset": "Ponastavi",
"label.save": "Shrani",
"label.settings": "Nastavitve",
"label.share-url": "Deli URL",
"label.single-day": "En dan",
"label.this-month": "Ta mesec",
"label.this-week": "Ta teden",
"label.this-year": "Letos",
"label.timezone": "Časovni pas",
"label.today": "Danes",
"label.tracking-code": "Koda za sledenje",
"label.unknown": "Neznano",
"label.username": "Uporabniško ime",
"label.view-details": "Prikaži podrobnosti",
"label.websites": "Spletna mesta",
"message.active-users": "{x} trenutni {x, plural, one {obiskovalec} other {obiskovalcev}}",
"message.confirm-delete": "Ste prepričani, da želite izbrisati {target}?",
"message.copied": "Kopirano!",
"message.delete-warning": "Izbrisani bodo tudi vsi povezani podatki.",
"message.failure": "Prišlo je do napake.",
"message.get-share-url": "Pridobi URL za skupno rabo",
"message.get-tracking-code": "Pridobi kodo za sledenje",
"message.go-to-settings": "Pojdi v nastavitve",
"message.incorrect-username-password": "Nepravilno uporabniško ime/geslo",
"message.log.visitor": "Obiskovalec iz {country} uporablja {browser} na {os} {device}",
"message.new-version-available": "Nova verzija umami {version} je na voljo!",
"message.no-data-available": "Podatki niso na voljo.",
"message.no-websites-configured": "Ni nastavljenih spletnih mest.",
"message.page-not-found": "Stran ni bila najdena.",
"message.powered-by": "Zagotavlja {name}",
"message.save-success": "Uspešno shranjeno.",
"message.share-url": "To je javno dostopen naslov URL za {target}.",
"message.track-stats": "Če želite spremljati statistične podatke za {target}, v {head} del vašega spletnega mesta namestite naslednjo kodo.",
"message.type-delete": "V spodnje polje vnesite {delete} za potrditev.",
"metrics.actions": "Dejanja",
"metrics.average-visit-time": "Povprečni čas obiska",
"metrics.bounce-rate": "Zapustna stopnja",
"metrics.browsers": "Brskalniki",
"metrics.countries": "Države",
"metrics.device.desktop": "Namizni računalnik",
"metrics.device.laptop": "Prenosni računalnik",
"metrics.device.mobile": "Mobilni telefon",
"metrics.device.tablet": "Tablični računalnik",
"metrics.devices": "Naprave",
"metrics.events": "Dogodki",
"metrics.filter.combined": "Skupno",
"metrics.filter.domain-only": "Samo domena",
"metrics.filter.raw": "Neobdelane meritve",
"metrics.operating-systems": "Operacijski sistemi",
"metrics.page-views": "Ogledi strani",
"metrics.pages": "Strani",
"metrics.referrers": "Viri",
"metrics.unique-visitors": "Unikatni obiskovalci",
"metrics.views": "Ogledi",
"metrics.visitors": "Obiskovalci"
}

View File

@ -25,6 +25,7 @@ import {
ptBR,
ro,
ru,
sl,
sv,
ta,
tr,
@ -66,6 +67,7 @@ export const languages = {
'ru-RU': { label: 'Русский', display: 'ru' },
'ro-RO': { label: 'Română', display: 'ro' },
'sk-SK': { label: 'Slovenčina', display: 'sk' },
'sl-SI': { label: 'Slovene', display: 'sl' },
'fi-FI': { label: 'Suomi', display: 'fi' },
'sv-SE': { label: 'Svenska', display: 'sv' },
'ta-IN': { label: 'தமிழ்', display: 'ta' },
@ -111,4 +113,5 @@ export const dateLocales = {
'ca-ES': ca,
'hu-HU': hu,
'ko-KR': ko,
'sl-SI': sl,
};

View File

@ -141,6 +141,10 @@ export async function updateWebsite(website_id, data) {
);
}
export async function resetWebsite(website_id) {
return runQuery(prisma.$queryRaw`delete from session where website_id=${website_id}`);
}
export async function deleteWebsite(website_id) {
return runQuery(
/* Prisma bug, does not cascade on non-nullable foreign keys

View File

@ -1,6 +1,6 @@
{
"name": "umami",
"version": "1.20.0",
"version": "1.22.0",
"description": "A simple, fast, website analytics alternative to Google Analytics. ",
"author": "Mike Cao <mike@mikecao.com>",
"license": "MIT",
@ -60,7 +60,7 @@
"@fontsource/noto-sans-jp": "^4.5.0",
"@fontsource/noto-sans-sc": "^4.5.0",
"@fontsource/noto-sans-tc": "^4.5.0",
"@prisma/client": "2.27.0",
"@prisma/client": "2.29.1",
"@reduxjs/toolkit": "^1.6.1",
"bcryptjs": "^2.4.3",
"chalk": "^4.1.1",
@ -128,7 +128,7 @@
"postcss-rtlcss": "^3.3.2",
"prettier": "^2.3.2",
"prettier-eslint": "^12.0.0",
"prisma": "2.27.0",
"prisma": "2.29.1",
"rollup": "^2.48.0",
"rollup-plugin-hashbang": "^2.2.2",
"rollup-plugin-terser": "^7.0.2",
@ -136,6 +136,6 @@
"stylelint-config-css-modules": "^2.2.0",
"stylelint-config-prettier": "^8.0.1",
"stylelint-config-recommended": "^5.0.0",
"tar": "^6.1.1"
"tar": "^6.1.2"
}
}

View File

@ -24,7 +24,7 @@ export default async (req, res) => {
const addr = ipaddr.parse(ip);
const range = ipaddr.parseCIDR(i);
if (addr.match(range)) return true;
if (addr.kind() === range[0].kind() && addr.match(range)) return true;
}
return false;

View File

@ -0,0 +1,20 @@
import { resetWebsite } from 'lib/queries';
import { methodNotAllowed, ok, unauthorized } from 'lib/response';
import { allowQuery } from 'lib/auth';
export default async (req, res) => {
const { id } = req.query;
const websiteId = +id;
if (req.method === 'POST') {
if (!(await allowQuery(req))) {
return unauthorized(res);
}
await resetWebsite(websiteId);
return ok(res);
}
return methodNotAllowed(res);
};

View File

@ -14,10 +14,18 @@ export default async (req, res) => {
const startDate = new Date(+start_at);
const endDate = new Date(+end_at);
const distance = end_at - start_at;
const prevStartDate = new Date(+start_at - distance);
const prevEndDate = new Date(+end_at - distance);
const metrics = await getWebsiteStats(websiteId, startDate, endDate, { url });
const prevPeriod = await getWebsiteStats(websiteId, prevStartDate, prevEndDate, { url });
const stats = Object.keys(metrics[0]).reduce((obj, key) => {
obj[key] = Number(metrics[0][key]) || 0;
obj[key] = {
value: Number(metrics[0][key]) || 0,
change: Number(metrics[0][key] - prevPeriod[0][key]) || 0,
};
return obj;
}, {});

View File

@ -0,0 +1 @@
{"AF":"Afganistan","AX":"\u00c5landski otoki","AL":"Albanija","DZ":"Al\u017eirija","AS":"Ameri\u0161ka Samoa","VI":"Ameri\u0161ki Devi\u0161ki otoki","AD":"Andora","AO":"Angola","AI":"Angvila","AQ":"Antarktika","AG":"Antigva in Barbuda","AR":"Argentina","AM":"Armenija","AW":"Aruba","AU":"Avstralija","AT":"Avstrija","AZ":"Azerbajd\u017ean","BS":"Bahami","BH":"Bahrajn","BD":"Banglade\u0161","BB":"Barbados","BE":"Belgija","BZ":"Belize","BY":"Belorusija","BJ":"Benin","BM":"Bermudi","BW":"Bocvana","BG":"Bolgarija","BO":"Bolivija","BA":"Bosna in Hercegovina","BV":"Bouvetov otok","CX":"Bo\u017ei\u010dni otok","BR":"Brazilija","VG":"Britanski Devi\u0161ki otoki","IO":"Britansko ozemlje v Indijskem oceanu","BN":"Brunej","BF":"Burkina Faso","BI":"Burundi","BT":"Butan","CF":"Centralnoafri\u0161ka republika","CY":"Ciper","CK":"Cookovi otoki","CW":"Cura\u00e7ao","TD":"\u010cad","CZ":"\u010ce\u0161ka","CL":"\u010cile","ME":"\u010crna gora","DK":"Danska","CD":"Demokrati\u010dna republika Kongo","DM":"Dominika","DO":"Dominikanska republika","DJ":"D\u017eibuti","EG":"Egipt","EC":"Ekvador","GQ":"Ekvatorialna Gvineja","ER":"Eritreja","EE":"Estonija","SZ":"Esvatini","ET":"Etiopija","FK":"Falklandski otoki","FO":"Ferski otoki","FJ":"Fid\u017ei","PH":"Filipini","FI":"Finska","FR":"Francija","GF":"Francoska Gvajana","PF":"Francoska Polinezija","TF":"Francosko ju\u017eno ozemlje","GA":"Gabon","GM":"Gambija","GH":"Gana","GI":"Gibraltar","GR":"Gr\u010dija","GD":"Grenada","GL":"Grenlandija","GE":"Gruzija","GP":"Guadeloupe","GU":"Guam","GG":"Guernsey","GY":"Gvajana","GT":"Gvatemala","GN":"Gvineja","GW":"Gvineja Bissau","HT":"Haiti","HM":"Heardov otok in McDonaldovi otoki","HN":"Honduras","HR":"Hrva\u0161ka","IN":"Indija","ID":"Indonezija","IQ":"Irak","IR":"Iran","IE":"Irska","IS":"Islandija","IT":"Italija","IL":"Izrael","JM":"Jamajka","JP":"Japonska","YE":"Jemen","JE":"Jersey","JO":"Jordanija","GS":"Ju\u017ena Georgia in Ju\u017eni Sandwichevi otoki","KR":"Ju\u017ena Koreja","SS":"Ju\u017eni Sudan","ZA":"Ju\u017enoafri\u0161ka republika","KY":"Kajmanski otoki","KH":"Kambod\u017ea","CM":"Kamerun","CA":"Kanada","QA":"Katar","KZ":"Kazahstan","KE":"Kenija","KG":"Kirgizistan","KI":"Kiribati","CN":"Kitajska","CC":"Kokosovi otoki","CO":"Kolumbija","KM":"Komori","CG":"Kongo - Brazzaville","CR":"Kostarika","CU":"Kuba","KW":"Kuvajt","LA":"Laos","LV":"Latvija","LS":"Lesoto","LB":"Libanon","LR":"Liberija","LY":"Libija","LI":"Lihten\u0161tajn","LT":"Litva","LU":"Luksemburg","MG":"Madagaskar","HU":"Mad\u017earska","MW":"Malavi","MV":"Maldivi","MY":"Malezija","ML":"Mali","MT":"Malta","MA":"Maroko","MH":"Marshallovi otoki","MQ":"Martinik","MU":"Mauritius","MR":"Mavretanija","YT":"Mayotte","MX":"Mehika","FM":"Mikronezija","MM":"Mjanmar (Burma)","MD":"Moldavija","MC":"Monako","MN":"Mongolija","MS":"Montserrat","MZ":"Mozambik","NA":"Namibija","NR":"Nauru","DE":"Nem\u010dija","NP":"Nepal","NE":"Niger","NG":"Nigerija","NI":"Nikaragva","NU":"Niue","NL":"Nizozemska","BQ":"Nizozemski Karibi","NF":"Norfol\u0161ki otok","NO":"Norve\u0161ka","NC":"Nova Kaledonija","NZ":"Nova Zelandija","OM":"Oman","IM":"Otok Man","TC":"Otoki Turks in Caicos","PK":"Pakistan","PW":"Palau","PS":"Palestinsko ozemlje","PA":"Panama","PG":"Papua Nova Gvineja","PY":"Paragvaj","PE":"Peru","PN":"Pitcairn","PL":"Poljska","PR":"Portoriko","PT":"Portugalska","HK":"Posebno administrativno obmo\u010dje LR Kitajske Hongkong","MO":"Posebno administrativno obmo\u010dje LR Kitajske Macao","RE":"Reunion","RO":"Romunija","RW":"Ruanda","RU":"Rusija","BL":"Saint Barth\u00e9lemy","KN":"Saint Kitts in Nevis","LC":"Saint Lucia","MF":"Saint Martin","PM":"Saint Pierre in Miquelon","VC":"Saint Vincent in Grenadine","SB":"Salomonovi otoki","SV":"Salvador","WS":"Samoa","SM":"San Marino","ST":"Sao Tome in Principe","SA":"Saudova Arabija","SC":"Sej\u0161eli","SN":"Senegal","KP":"Severna Koreja","MK":"Severna Makedonija","MP":"Severni Marianski otoki","SL":"Sierra Leone","SG":"Singapur","SX":"Sint Maarten","SY":"Sirija","CI":"Slonoko\u0161\u010dena obala","SK":"Slova\u0161ka","SI":"Slovenija","SO":"Somalija","RS":"Srbija","UM":"Stranski zunanji otoki Zdru\u017eenih dr\u017eav","SD":"Sudan","SR":"Surinam","SJ":"Svalbard in Jan Mayen","SH":"Sveta Helena","ES":"\u0160panija","LK":"\u0160rilanka","SE":"\u0160vedska","CH":"\u0160vica","TJ":"Tad\u017eikistan","TH":"Tajska","TW":"Tajvan","TZ":"Tanzanija","TL":"Timor-Leste","TG":"Togo","TK":"Tokelau","TO":"Tonga","TT":"Trinidad in Tobago","TN":"Tunizija","TR":"Tur\u010dija","TM":"Turkmenistan","TV":"Tuvalu","UG":"Uganda","UA":"Ukrajina","UY":"Urugvaj","UZ":"Uzbekistan","VU":"Vanuatu","VA":"Vatikan","VE":"Venezuela","VN":"Vietnam","WF":"Wallis in Futuna","EH":"Zahodna Sahara","ZM":"Zambija","US":"Zdru\u017eene dr\u017eave Amerike","AE":"Zdru\u017eeni arabski emirati","GB":"Zdru\u017eeno kraljestvo","CV":"Zelenortski otoki","ZW":"Zimbabve"}

1278
yarn.lock

File diff suppressed because it is too large Load Diff