Merge pull request #2723 from umami-software/release

v2.11.3
This commit is contained in:
Mike Cao
2024-05-03 21:35:55 -07:00
committed by GitHub
61 changed files with 3744 additions and 2183 deletions
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "umami",
"version": "2.11.2",
"version": "2.11.3",
"description": "A simple, fast, privacy-focused alternative to Google Analytics.",
"author": "Umami Software, Inc. <hello@umami.is>",
"license": "MIT",
@@ -71,7 +71,7 @@
"@react-spring/web": "^9.7.3",
"@tanstack/react-query": "^5.28.6",
"@umami/prisma-client": "^0.14.0",
"@umami/redis-client": "^0.18.0",
"@umami/redis-client": "^0.20.0",
"chalk": "^4.1.1",
"chart.js": "^4.4.2",
"chartjs-adapter-date-fns": "^3.0.0",
@@ -121,7 +121,7 @@
},
"devDependencies": {
"@formatjs/cli": "^4.2.29",
"@netlify/plugin-nextjs": "^4.41.3",
"@netlify/plugin-nextjs": "^5.1.0",
"@rollup/plugin-alias": "^5.0.0",
"@rollup/plugin-commonjs": "^25.0.4",
"@rollup/plugin-json": "^6.0.0",
+251
View File
@@ -0,0 +1,251 @@
{
"AF": "Afganistan",
"AL": "Albanija",
"DZ": "Al\u017eir",
"VI": "Ameri\u010dka Djevi\u010danska ostrva",
"AS": "Ameri\u010dka Samoa",
"UM": "Ameri\u010dka Vanjska Ostrva",
"AD": "Andora",
"AO": "Angola",
"AI": "Angvila",
"AQ": "Antarktika",
"AG": "Antigva i Barbuda",
"AR": "Argentina",
"AM": "Armenija",
"AW": "Aruba",
"AU": "Australija",
"AT": "Austrija",
"AZ": "Azerbejd\u017ean",
"BS": "Bahami",
"BH": "Bahrein",
"BD": "Banglade\u0161",
"BB": "Barbados",
"BE": "Belgija",
"BZ": "Belize",
"BJ": "Benin",
"BM": "Bermuda",
"BY": "Bjelorusija",
"BW": "Bocvana",
"BO": "Bolivija",
"BA": "Bosna i Hercegovina",
"CX": "Bo\u017ei\u0107no ostrvo",
"BR": "Brazil",
"VG": "Britanska Djevi\u010danska ostrva",
"IO": "Britanska Teritorija u Indijskom Okeanu",
"BN": "Brunej",
"BG": "Bugarska",
"BF": "Burkina Faso",
"BI": "Burundi",
"BT": "Butan",
"CF": "Centralnoafri\u010dka Republika",
"ME": "Crna Gora",
"TD": "\u010cad",
"CZ": "\u010ce\u0161ka",
"CL": "\u010cile",
"DK": "Danska",
"CD": "Demokratska Republika Kongo",
"DM": "Dominika",
"DO": "Dominikanska Republika",
"DJ": "D\u017eibuti",
"EG": "Egipat",
"EC": "Ekvador",
"GQ": "Ekvatorijalna Gvineja",
"ER": "Eritreja",
"EE": "Estonija",
"SZ": "Esvatini",
"ET": "Etiopija",
"FO": "Farska ostrva",
"FJ": "Fid\u017ei",
"PH": "Filipini",
"FI": "Finska",
"FK": "Folklandska ostrva",
"FR": "Francuska",
"GF": "Francuska Gvajana",
"PF": "Francuska Polinezija",
"TF": "Francuske Ju\u017ene Teritorije",
"GA": "Gabon",
"GM": "Gambija",
"GH": "Gana",
"GG": "Gernzi",
"GI": "Gibraltar",
"GR": "Gr\u010dka",
"GD": "Grenada",
"GL": "Grenland",
"GE": "Gruzija",
"GU": "Guam",
"GP": "Gvadalupe",
"GY": "Gvajana",
"GT": "Gvatemala",
"GN": "Gvineja",
"GW": "Gvineja-Bisao",
"HT": "Haiti",
"HM": "Herd i arhipelag MekDonald",
"NL": "Holandija",
"HN": "Honduras",
"HK": "Hong Kong (SAR Kina)",
"HR": "Hrvatska",
"IN": "Indija",
"ID": "Indonezija",
"IQ": "Irak",
"IR": "Iran",
"IE": "Irska",
"IS": "Island",
"TL": "Isto\u010dni Timor",
"IT": "Italija",
"IL": "Izrael",
"JM": "Jamajka",
"JP": "Japan",
"YE": "Jemen",
"JE": "Jersey",
"JO": "Jordan",
"GS": "Ju\u017ena D\u017eord\u017eija i Ju\u017ena Sendvi\u010d ostrva",
"KR": "Ju\u017ena Koreja",
"SS": "Ju\u017eni Sudan",
"ZA": "Ju\u017enoafri\u010dka Republika",
"KY": "Kajmanska ostrva",
"KH": "Kambod\u017ea",
"CM": "Kamerun",
"CA": "Kanada",
"CV": "Kape Verde",
"BQ": "Karipska Holandija",
"QA": "Katar",
"KZ": "Kazahstan",
"KE": "Kenija",
"CN": "Kina",
"CY": "Kipar",
"KG": "Kirgistan",
"KI": "Kiribati",
"CC": "Kokosova (Keelingova) ostrva",
"CO": "Kolumbija",
"KM": "Komori",
"CG": "Kongo",
"CR": "Kostarika",
"CU": "Kuba",
"CK": "Kukova ostrva",
"CW": "Kurasao",
"KW": "Kuvajt",
"LA": "Laos",
"LV": "Latvija",
"LS": "Lesoto",
"LB": "Liban",
"LR": "Liberija",
"LY": "Libija",
"LI": "Lihten\u0161tajn",
"LT": "Litvanija",
"LU": "Luksemburg",
"MG": "Madagaskar",
"HU": "Ma\u0111arska",
"YT": "Majote",
"MO": "Makao (SAR Kina)",
"MW": "Malavi",
"MV": "Maldivi",
"MY": "Malezija",
"ML": "Mali",
"MT": "Malta",
"MA": "Maroko",
"MH": "Mar\u0161alova ostrva",
"MQ": "Martinik",
"MU": "Mauricijus",
"MR": "Mauritanija",
"MX": "Meksiko",
"FM": "Mikronezija",
"MM": "Mjanmar",
"MD": "Moldavija",
"MC": "Monako",
"MN": "Mongolija",
"MS": "Monserat",
"MZ": "Mozambik",
"NA": "Namibija",
"NR": "Nauru",
"NP": "Nepal",
"NE": "Niger",
"NG": "Nigerija",
"NI": "Nikaragva",
"NU": "Niue",
"NO": "Norve\u0161ka",
"NC": "Nova Kaledonija",
"NZ": "Novi Zeland",
"DE": "Njema\u010dka",
"CI": "Obala Slonova\u010de",
"AX": "Olandska ostrva",
"OM": "Oman",
"TC": "Ostrva Turks i Kaikos",
"WF": "Ostrva Valis i Futuna",
"BV": "Ostrvo Buve",
"IM": "Ostrvo Man",
"NF": "Ostrvo Norfolk",
"PK": "Pakistan",
"PW": "Palau",
"PS": "Palestinska Teritorija",
"PA": "Panama",
"PG": "Papua Nova Gvineja",
"PY": "Paragvaj",
"PE": "Peru",
"PN": "Pitkernska Ostrva",
"PL": "Poljska",
"PR": "Porto Riko",
"PT": "Portugal",
"RE": "Reunion",
"RW": "Ruanda",
"RO": "Rumunija",
"RU": "Rusija",
"SV": "Salvador",
"WS": "Samoa",
"SM": "San Marino",
"ST": "Sao Tome i Principe",
"SA": "Saudijska Arabija",
"SC": "Sej\u0161eli",
"SN": "Senegal",
"SL": "Sijera Leone",
"SG": "Singapur",
"SX": "Sint Marten",
"SY": "Sirija",
"US": "Sjedinjene Dr\u017eave",
"KP": "Sjeverna Koreja",
"MK": "Sjeverna Makedonija",
"MP": "Sjeverna Marijanska ostrva",
"SK": "Slova\u010dka",
"SI": "Slovenija",
"SB": "Solomonska Ostrva",
"SO": "Somalija",
"RS": "Srbija",
"SD": "Sudan",
"SR": "Surinam",
"SJ": "Svalbard i Jan Majen",
"SH": "Sveta Helena",
"LC": "Sveta Lucija",
"BL": "Sveti Bartolomej",
"KN": "Sveti Kits i Nevis",
"MF": "Sveti Martin",
"PM": "Sveti Petar i Mikelon",
"VC": "Sveti Vinsent i Grenadin",
"ES": "\u0160panija",
"LK": "\u0160ri Lanka",
"SE": "\u0160vedska",
"CH": "\u0160vicarska",
"TJ": "Tad\u017eikistan",
"TH": "Tajland",
"TW": "Tajvan",
"TZ": "Tanzanija",
"TG": "Togo",
"TK": "Tokelau",
"TO": "Tonga",
"TT": "Trinidad i Tobago",
"TN": "Tunis",
"TM": "Turkmenistan",
"TR": "Turska",
"TV": "Tuvalu",
"UG": "Uganda",
"AE": "Ujedinjeni Arapski Emirati",
"GB": "Ujedinjeno Kraljevstvo",
"UA": "Ukrajina",
"UY": "Urugvaj",
"UZ": "Uzbekistan",
"VU": "Vanuatu",
"VA": "Vatikan",
"VE": "Venecuela",
"VN": "Vijetnam",
"ZM": "Zambija",
"EH": "Zapadna Sahara",
"ZW": "Zimbabve"
}
+611
View File
@@ -0,0 +1,611 @@
{
"ab": "abhazijski",
"ace": "a\u010dineski",
"ada": "adangmejski",
"ady": "adigejski",
"aa": "afarski",
"afh": "afrihili",
"af": "afrikanerski",
"agq": "aghem",
"ain": "ainu",
"ay": "ajmara",
"akk": "akadijski",
"ak": "akan",
"ach": "akoli",
"bss": "Akoose",
"akz": "Alabama",
"sq": "albanski",
"arq": "Algerian Arabic",
"ale": "aljut",
"ase": "American Sign Language",
"en_US": "ameri\u010dki engleski",
"am": "amharski",
"anp": "angika",
"njo": "Ao Naga",
"an": "aragone\u017eanski",
"aro": "Araona",
"arp": "arapaho",
"ar": "arapski",
"arn": "araukanski",
"arw": "aravak",
"arc": "armajski",
"rup": "aromanijski",
"frp": "Arpitan",
"as": "asemijski",
"ast": "asturijski",
"asa": "asu",
"cch": "atsam",
"en_AU": "australski engleski",
"de_AT": "austrijski njema\u010dki",
"awa": "avadhi",
"av": "avarski",
"ae": "avestanski",
"az": "azerbejd\u017eanski",
"bfq": "Badaga",
"ksf": "bafia",
"bfd": "Bafut",
"bqi": "Bakhtiari",
"ban": "balinezijski",
"bal": "balu\u010di",
"bm": "bambara",
"bax": "Bamun",
"bjn": "Banjar",
"bas": "basa",
"eu": "baskijski",
"ba": "ba\u0161kirski",
"bbc": "Batak Toba",
"bar": "Bavarian",
"bej": "beja",
"bem": "bemba",
"bez": "bena",
"bn": "bengalski",
"bew": "Betawi",
"zxx": "bez lingvisti\u010dkog sadr\u017eaja",
"bik": "bikol",
"bin": "bini",
"bpy": "Bishnupriya",
"bi": "bislama",
"be": "bjeloruski",
"byn": "blin",
"zbl": "blisimboli",
"brx": "bodo",
"bho": "bojpuri",
"bs": "bosanski",
"brh": "Brahui",
"bra": "braj",
"pt_BR": "Brazilian Portuguese",
"br": "bretonski",
"en_GB": "britanski engleski",
"bg": "bugarski",
"bug": "bugine\u017eanskii",
"bum": "Bulu",
"bua": "buriat",
"my": "burmanski",
"frc": "Cajun French",
"yue": "Cantonese",
"cps": "Capiznon",
"cay": "Cayuga",
"ceb": "cebuano",
"dtp": "Central Dusun",
"esu": "Central Yupik",
"shu": "Chadian Arabic",
"qug": "Chimborazo Highland Quichua",
"ksh": "Colognian",
"swb": "Comorian",
"cy": "cy",
"chg": "\u010dagatai",
"ch": "\u010damoro",
"ce": "\u010de\u010denski",
"chy": "\u010dejenski",
"cs": "\u010de\u0161ki",
"chb": "\u010dib\u010da",
"cgg": "\u010diga",
"chn": "\u010dinukski",
"chp": "\u010dipvijanski",
"chr": "\u010diroki",
"cho": "\u010doktavski",
"chk": "\u010dukeski",
"cv": "\u010duva\u0161ki",
"dak": "dakota",
"da": "danski",
"dar": "dargva",
"dzg": "Dazaga",
"del": "delaver",
"din": "dinka",
"dv": "divehijski",
"doi": "dogri",
"dgr": "dogrib",
"dsb": "donjolu\u017ei\u010dkosrpski",
"dua": "duala",
"gez": "d\u017eiz",
"dz": "d\u017eonga",
"dyu": "\u0111ula",
"efi": "efikski",
"arz": "Egyptian Arabic",
"eka": "ekajuk",
"elx": "elamitski",
"ebu": "embu",
"egl": "Emilian",
"en": "engleski",
"myv": "erzija",
"eo": "esperanto",
"et": "estonski",
"pt_PT": "European Portuguese",
"ee": "eve",
"ewo": "evondo",
"es_ES": "evropski \u0161panski",
"ext": "Extremaduran",
"fan": "fang",
"fat": "fanti",
"fo": "farski",
"phn": "feni\u010danski",
"fj": "fid\u017eijski",
"hif": "Fiji Hindi",
"fil": "filipinski",
"fi": "finski",
"nl_BE": "flamanski",
"fon": "fon",
"gur": "Frafra",
"fr": "francuski",
"fur": "friulijski",
"fy": "frizijski",
"ff": "fulah",
"gaa": "ga",
"gag": "gagau\u0161ki",
"gay": "gajo",
"gl": "galski",
"gan": "Gan Chinese",
"lg": "ganda",
"gba": "gbaja",
"aln": "Gheg Albanian",
"bbj": "Ghomala",
"glk": "Gilaki",
"gil": "gilbert\u0161ki",
"gom": "Goan Konkani",
"gon": "gondi",
"hsb": "gornjolu\u017ei\u010dkosrpski",
"de_CH": "gornjonjema\u010dki (\u0161vicarski)",
"gor": "gorontalo",
"got": "gotski",
"el": "gr\u010dki",
"grb": "grebo",
"ka": "gruzijski",
"gu": "gud\u017earati",
"guz": "gusii",
"gn": "gvarani",
"gwi": "Gwich\u02bcin",
"ht": "hai\u0107anski",
"hai": "haida",
"hak": "Hakka Chinese",
"ha": "hausa",
"haw": "havajski",
"he": "hebrejski",
"hz": "herero",
"hil": "hiligajnon",
"hi": "hindi",
"ho": "hiri motu",
"hit": "hitite",
"hmn": "hmong",
"nl": "holandski",
"hr": "hrvatski",
"hup": "hupa",
"iba": "iban",
"ibb": "Ibibio",
"io": "ido",
"ig": "igbo",
"ilo": "iloko",
"smn": "inari sami",
"id": "indonezijski",
"izh": "Ingrian",
"inh": "ingu\u0161etski",
"ia": "interlingva",
"ie": "interlingve",
"iu": "inuktitut",
"ik": "inupiak",
"ga": "irski",
"is": "islandski",
"frs": "isto\u010dni frizijski",
"it": "italijanski",
"sah": "jakut",
"jam": "Jamaican Creole English",
"yao": "jao",
"ja": "japanski",
"yap": "jape\u0161ki",
"jv": "javanski",
"hy": "jermenski",
"yi": "jidi\u0161",
"dyo": "jola-fonyi",
"yo": "jorubanski",
"jrb": "judeo-arapski",
"jpr": "judeo-persijski",
"jut": "Jutish",
"alt": "ju\u017eni altai",
"nr": "ju\u017eni ndebele",
"sma": "ju\u017eni sami",
"kbd": "kabardijski",
"kab": "kabile",
"kac": "ka\u010din",
"cad": "kado",
"kgp": "Kaingang",
"kkj": "Kako",
"kl": "kalalisutski",
"kln": "kalenjin",
"xal": "kalmik",
"kam": "kamba",
"kn": "kanada",
"en_CA": "kanadski engleski",
"fr_CA": "kanadski francuski",
"kbl": "Kanembu",
"kr": "kanuri",
"kaa": "kara-kalpa\u0161ki",
"krc": "kara\u010daj-balkar",
"krl": "karelijski",
"car": "karipski",
"kha": "kasi",
"ks": "ka\u0161miri",
"csb": "ka\u0161ubijanski",
"ca": "katalonski",
"kaw": "kavi",
"kk": "kaza\u010dki",
"ken": "Kenyang",
"khw": "Khowar",
"quc": "ki\u010de",
"ki": "kikuju",
"kmb": "kimbundu",
"krj": "Kinaray-a",
"zh": "kineski",
"zh_Hans": "kineski (pojednostavljeni)",
"zh_Hant": "kineski (tradicionalni)",
"rw": "kinjarvanda",
"ky": "kirgiski",
"kiu": "Kirmanjki",
"nwc": "klasi\u010dni nevari",
"syc": "klasi\u010dni sirijski",
"tlh": "klingonski",
"km": "kmerski",
"ses": "kojraboro seni",
"bkm": "Kom",
"kv": "komi",
"koi": "komi-permja\u010dki",
"kg": "kongo",
"swc": "kongoanski swahili",
"kok": "konkani",
"cop": "koptski",
"ko": "korejski",
"kw": "korni\u0161ki",
"kfo": "koro",
"co": "korzikanski",
"xh": "kosa",
"kos": "kosreanski",
"kho": "kotanizijski",
"avk": "Kotava",
"khq": "koyra chiini",
"kpe": "kpele",
"cr": "kri",
"crh": "krimeanski turski",
"kri": "Krio",
"mus": "kri\u0161ki",
"kj": "kuanjama",
"kum": "kumik",
"ku": "kurdski",
"kru": "kurukh",
"kut": "kutenai",
"qu": "kven\u010da",
"nmg": "kwasio",
"lad": "ladino",
"lkt": "lakota",
"lam": "lamba",
"lah": "landa",
"lag": "langi",
"lo": "lao\u0161ki",
"ltg": "Latgalian",
"es_419": "latinoameri\u010dki \u0161panski",
"la": "latinski",
"lzz": "Laz",
"lv": "letonski",
"lez": "lezgian",
"lij": "Ligurian",
"li": "limburgi\u0161",
"ln": "lingala",
"lfn": "Lingua Franca Nova",
"lzh": "Literary Chinese",
"lt": "litvanski",
"liv": "Livonian",
"jbo": "lojban",
"lmo": "Lombard",
"sli": "Lower Silesian",
"loz": "lozi",
"lu": "luba-katanga",
"lua": "luba-lulua",
"lui": "luiseno",
"lb": "luksembur\u0161ki",
"smj": "lule sami",
"lun": "lunda",
"luo": "luo",
"lus": "lu\u0161ai",
"luy": "luyia",
"mde": "Maba",
"jmc": "machame",
"mad": "madure\u0161ki",
"hu": "ma\u0111arski",
"maf": "Mafa",
"mag": "magahi",
"moh": "mahavski",
"vmf": "Main-Franconian",
"mai": "maitili",
"mak": "makasar",
"mk": "makedonski",
"mgh": "makhuwa-meetto",
"kde": "makonde",
"mg": "malagazijski",
"ml": "malajalam",
"ms": "malajski",
"mt": "malte\u0161ki",
"mnc": "man\u010du",
"mdr": "mandar",
"man": "mandingo",
"mni": "manipuri",
"gv": "manks",
"mi": "maorski",
"mr": "marati",
"chm": "mari",
"tzm": "marokanski tamazigt",
"mh": "mar\u0161alski",
"mwr": "marvari",
"mas": "masai",
"mfe": "mauricijski kreolski",
"mzn": "Mazanderani",
"byv": "Medumba",
"es_MX": "meksi\u010dki \u0161panski",
"men": "mende",
"mwv": "Mentawai",
"mer": "meru",
"mgo": "meta\u2019",
"mic": "mikmak",
"nan": "Min Nan Chinese",
"min": "minangkabau",
"xmf": "Mingrelian",
"mwl": "mirande\u0161ki",
"ar_001": "moderni standardni arapski",
"mdf": "mok\u0161a",
"ro_MD": "moldavski",
"lol": "mongo",
"mn": "mongolski",
"ary": "Moroccan Arabic",
"mos": "mosi",
"mua": "mundang",
"ttt": "Muslim Tat",
"mye": "Myene",
"naq": "nama",
"na": "nauru",
"nv": "navaho",
"ng": "ndonga",
"nap": "neapolitanski",
"ne": "nepalski",
"und": "nepoznati ili neva\u017ee\u0107i jezik",
"new": "nevari",
"sba": "Ngambay",
"nnh": "Ngiemboon",
"jgo": "ngomba",
"yrl": "Nheengatu",
"nia": "nias",
"nds": "niski nema\u010dki",
"niu": "niuean",
"nqo": "nko",
"nog": "nogai",
"no": "norve\u0161ki",
"nb": "norve\u0161ki bokmal",
"nn": "norve\u0161ki njorsk",
"nov": "Novial",
"nus": "nuer",
"nzi": "nzima",
"nym": "njamvezi",
"nyn": "njankole",
"ny": "njanja",
"tog": "njasa tonga",
"de": "njema\u010dki",
"nyo": "njoro",
"oj": "ojibva",
"or": "orijski",
"om": "oromo",
"osa": "osage",
"os": "osetski",
"ota": "otomanski turski",
"pal": "pahlavi",
"pfl": "Palatine German",
"pau": "palauanski",
"pi": "pali",
"pam": "pampanga",
"pa": "pand\u017eabski",
"pag": "pangasinski",
"pap": "papiamento",
"ps": "pa\u0161tunski",
"pdc": "Pennsylvania German",
"fa": "perzijski",
"pcd": "Picard",
"pms": "Piedmontese",
"pdt": "Plautdietsch",
"pl": "poljski",
"pon": "ponpejski",
"pnt": "Pontic",
"pt": "portugalski",
"oc": "provansalski",
"prg": "Prussian",
"raj": "ra\u0111astani",
"rap": "rapanui",
"rar": "rarotongan",
"rm": "reto-romanski",
"rif": "Riffian",
"rgn": "Romagnol",
"rom": "romani",
"rof": "rombo",
"rtm": "Rotuman",
"rug": "Roviana",
"rwk": "rua",
"ro": "rumunski",
"root": "run",
"rn": "rundi",
"ru": "ruski",
"rue": "Rusyn",
"ssy": "Saho",
"sam": "samaritanski aramejski",
"saq": "samburu",
"sm": "samoanski",
"sgs": "Samogitian",
"sad": "sandave",
"sg": "sango",
"sbp": "sangu",
"sa": "sanskrit",
"sat": "santali",
"sc": "sardinijski",
"sas": "sasak",
"sdc": "Sassarese Sardinian",
"stq": "Saterland Frisian",
"saz": "Saurashtra",
"sly": "Selayar",
"sel": "selkap",
"seh": "sena",
"see": "Seneca",
"srr": "serer",
"sei": "Seri",
"st": "sesoto",
"nso": "severni soto",
"frr": "severno-frizijski",
"ksb": "shambala",
"scn": "sicilijanski",
"ii": "si\u010duan ji",
"sid": "sidamo",
"bla": "siksika",
"szl": "Silesian",
"sd": "sindi",
"si": "singaleski",
"syr": "sirijski",
"nd": "sjeverni ndebele",
"se": "sjeverni sami",
"sms": "skoltski jezik",
"den": "slavski",
"sk": "slova\u010dki",
"sl": "slovena\u010dki",
"sog": "sod\u017eijenski",
"xog": "soga",
"so": "somalski",
"snk": "soninke",
"ckb": "soranski kurdski",
"azb": "South Azerbaijani",
"srn": "srananski tongo",
"enm": "srednji engleski",
"frm": "srednji francuski",
"dum": "srednji holandski",
"mga": "srednji irski",
"gmh": "srednji visoki nema\u010dki",
"sr": "srpski",
"sh": "srpskohrvatski",
"zgh": "standardni marokanski tamazigt",
"non": "stari norski",
"egy": "staroegipatski",
"ang": "staroengleski",
"fro": "starofrancuski",
"grc": "starogr\u010dki",
"sga": "staroirski",
"goh": "staronema\u010dki",
"peo": "staropersijski",
"pro": "staroprovansalski",
"cu": "staroslovenski",
"su": "sudanski",
"suk": "sukuma",
"sux": "sumerski",
"sus": "susu",
"sw": "svahili",
"ss": "svati",
"shn": "\u0161an",
"sco": "\u0161kotski",
"gd": "\u0161kotski galski",
"sn": "\u0161ona",
"es": "\u0161panski",
"fr_CH": "\u0161vajcarski francuski",
"gsw": "\u0161vajcarski njema\u010dki",
"sv": "\u0161vedski",
"tg": "tad\u017ei\u010dki",
"tl": "tagalski",
"shi": "tahelhit",
"ty": "tahi\u0107anski",
"dav": "taita",
"th": "tajlandski",
"tly": "Talysh",
"tmh": "tama\u0161ek",
"ta": "tamilski",
"trv": "Taroko",
"twq": "tasavak",
"tt": "tatarski",
"te": "telugu",
"ter": "tereno",
"teo": "teso",
"tet": "tetum",
"bo": "tibetanski",
"tig": "tigre",
"ti": "tigrinja",
"tem": "timne",
"tiv": "tiv",
"kcg": "tjap",
"tli": "tlingit",
"tpi": "tok pisin",
"tkl": "tokelau",
"to": "tonga",
"fit": "Tornedalen Finnish",
"tkr": "Tsakhur",
"tsd": "Tsakonian",
"tsi": "tsim\u0161ian",
"ts": "tsonga",
"tn": "tsvana",
"tcy": "Tulu",
"tum": "tumbuka",
"aeb": "Tunisian Arabic",
"tk": "turkmenski",
"tru": "Turoyo",
"tr": "turski",
"tvl": "tuvalu",
"tyv": "tuvinijski",
"tw": "tvi",
"udm": "udmurt",
"uga": "ugaritski",
"ug": "ujgurski",
"uk": "ukrajinski",
"umb": "umbundu",
"ur": "urdu",
"uz": "uzbe\u010dki",
"vai": "vai",
"wal": "valamo",
"wa": "valun",
"war": "varej",
"was": "va\u0161o",
"ve": "venda",
"vec": "Venetian",
"vep": "Veps",
"vi": "vijetnamski",
"mul": "vi\u0161e jezika",
"vo": "volap\u00fck",
"wo": "volof",
"vro": "V\u00f5ro",
"vot": "votski",
"vun": "vunjo",
"wae": "Walser",
"wbp": "Warlpiri",
"guc": "Wayuu",
"vls": "West Flemish",
"mrj": "Western Mari",
"wuu": "Wu Chinese",
"hsn": "Xiang Chinese",
"yav": "Yangben",
"ybb": "Yemba",
"zap": "zapote\u010dki",
"dje": "zarma",
"zza": "zaza",
"zea": "Zeelandic",
"kea": "zelenortski",
"zen": "zenaga",
"gbz": "Zoroastrian Dari",
"za": "zuang",
"zu": "zulu",
"zun": "zuni",
"kaj": "\u017eju"
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+8 -8
View File
@@ -38,7 +38,7 @@
"label.add-step": [
{
"type": 0,
"value": "Add step"
"value": "添加步骤"
}
],
"label.add-website": [
@@ -564,7 +564,7 @@
"label.last-months": [
{
"type": 0,
"value": "Last "
"value": "最近 "
},
{
"type": 1,
@@ -572,7 +572,7 @@
},
{
"type": 0,
"value": " months"
"value": " 个月"
}
],
"label.leave": [
@@ -996,7 +996,7 @@
"label.steps": [
{
"type": 0,
"value": "Steps"
"value": "步骤"
}
],
"label.sum": [
@@ -1176,7 +1176,7 @@
"label.update": [
{
"type": 0,
"value": "Update"
"value": "更新"
}
],
"label.url": [
@@ -1218,7 +1218,7 @@
"label.utm-description": [
{
"type": 0,
"value": "Track your campaigns through UTM parameters."
"value": "通过UTM参数追踪您的广告活动。"
}
],
"label.value": [
@@ -1254,7 +1254,7 @@
"label.views-per-visit": [
{
"type": 0,
"value": "Views per visit"
"value": "每次访问的浏览量"
}
],
"label.visitors": [
@@ -1266,7 +1266,7 @@
"label.visits": [
{
"type": 0,
"value": "Visits"
"value": "访问次数"
}
],
"label.website": [
+1 -5
View File
@@ -23,13 +23,9 @@ if (!process.env.SKIP_DB_CHECK && !process.env.DATABASE_TYPE) {
}
if (process.env.CLICKHOUSE_URL) {
checkMissing(['CA_CERT', 'CLIENT_CERT', 'CLIENT_KEY', 'KAFKA_BROKER', 'KAFKA_URL', 'REDIS_URL']);
checkMissing(['KAFKA_BROKER', 'KAFKA_URL', 'REDIS_URL']);
}
if (process.env.CLOUD_MODE) {
checkMissing(['CLOUD_URL']);
}
if (process.env.ENABLE_BLOCKER) {
checkMissing(['REDIS_URL']);
}
@@ -0,0 +1,3 @@
import Page from 'app/(main)/reports/utm/page';
export default Page;
@@ -0,0 +1,3 @@
import Page from 'app/(main)/websites/[websiteId]/event-data/page';
export default Page;
@@ -0,0 +1,3 @@
import Page from 'app/(main)/websites/[websiteId]/realtime/page';
export default Page;
@@ -0,0 +1,3 @@
import Page from 'app/(main)/websites/[websiteId]/reports/page';
export default Page;
@@ -1,6 +1,6 @@
import classNames from 'classnames';
import Favicon from 'components/common/Favicon';
import { useMessages, useWebsite } from 'components/hooks';
import { useMessages, useTeamUrl, useWebsite } from 'components/hooks';
import Icons from 'components/icons';
import ActiveUsers from 'components/metrics/ActiveUsers';
import Link from 'next/link';
@@ -19,6 +19,7 @@ export function WebsiteHeader({
children?: ReactNode;
}) {
const { formatMessage, labels } = useMessages();
const { renderTeamUrl } = useTeamUrl();
const pathname = usePathname();
const { data: website } = useWebsite(websiteId);
const { name, domain } = website || {};
@@ -62,7 +63,11 @@ export function WebsiteHeader({
: pathname.match(/^\/websites\/[\w-]+$/);
return (
<Link key={label} href={`/websites/${websiteId}${path}`} shallow={true}>
<Link
key={label}
href={renderTeamUrl(`/websites/${websiteId}${path}`)}
shallow={true}
>
<Button
variant="quiet"
className={classNames({
@@ -1,18 +1,19 @@
'use client';
import Link from 'next/link';
import { Button, Flexbox, Icon, Icons, Text } from 'react-basics';
import { useMessages } from 'components/hooks';
import { useMessages, useTeamUrl } from 'components/hooks';
import WebsiteHeader from '../WebsiteHeader';
import ReportsDataTable from 'app/(main)/reports/ReportsDataTable';
export function WebsiteReportsPage({ websiteId }) {
const { formatMessage, labels } = useMessages();
const { renderTeamUrl } = useTeamUrl();
return (
<>
<WebsiteHeader websiteId={websiteId} />
<Flexbox alignItems="center" justifyContent="end">
<Link href={`/reports/create`}>
<Link href={renderTeamUrl('/reports/create')}>
<Button variant="primary">
<Icon>
<Icons.Plus />
@@ -1,14 +1,14 @@
import { UseQueryOptions } from '@tanstack/react-query';
import { useState } from 'react';
import { useApi } from './useApi';
import { FilterResult, SearchFilter, FilterQueryResult } from 'lib/types';
import { PageResult, PageParams, FilterQueryResult } from 'lib/types';
export function useFilterQuery<T = any>({
queryKey,
queryFn,
...options
}: Omit<UseQueryOptions, 'queryFn'> & { queryFn: (params?: object) => any }): FilterQueryResult<T> {
const [params, setParams] = useState<T | SearchFilter>({
const [params, setParams] = useState<T | PageParams>({
query: '',
page: 1,
});
@@ -21,7 +21,7 @@ export function useFilterQuery<T = any>({
});
return {
result: data as FilterResult<any>,
result: data as PageResult<any>,
query,
params,
setParams,
@@ -1,33 +1,17 @@
import useApi from './useApi';
import { useFilterParams } from '../useFilterParams';
import { UseQueryOptions } from '@tanstack/react-query';
import { useDateRange, useNavigation, useTimezone } from 'components/hooks';
import { zonedTimeToUtc } from 'date-fns-tz';
export function useWebsiteEvents(
websiteId: string,
options?: Omit<UseQueryOptions, 'queryKey' | 'queryFn'>,
) {
const { get, useQuery } = useApi();
const [dateRange] = useDateRange(websiteId);
const { startDate, endDate, unit, offset } = dateRange;
const { timezone } = useTimezone();
const {
query: { url, event },
} = useNavigation();
const params = {
startAt: +zonedTimeToUtc(startDate, timezone),
endAt: +zonedTimeToUtc(endDate, timezone),
unit,
offset,
timezone,
url,
event,
};
const params = useFilterParams(websiteId);
return useQuery({
queryKey: ['events', { ...params }],
queryFn: () => get(`/websites/${websiteId}/events`, { ...params }),
queryKey: ['websites:events', { websiteId, ...params }],
queryFn: () => get(`/websites/${websiteId}/events`, params),
enabled: !!websiteId,
...options,
});
@@ -1,12 +1,15 @@
import useApi from './useApi';
import { UseQueryOptions } from '@tanstack/react-query';
import useApi from './useApi';
import { useFilterParams } from '../useFilterParams';
export function useWebsiteMetrics(
websiteId: string,
params?: { [key: string]: any },
type: string,
limit: number,
options?: Omit<UseQueryOptions & { onDataLoad?: (data: any) => void }, 'queryKey' | 'queryFn'>,
) {
const { get, useQuery } = useApi();
const params = useFilterParams(websiteId);
return useQuery({
queryKey: [
@@ -14,21 +17,26 @@ export function useWebsiteMetrics(
{
websiteId,
...params,
type,
limit,
},
],
queryFn: async () => {
const filters = { ...params };
filters[params.type] = undefined;
filters[type] = undefined;
const data = await get(`/websites/${websiteId}/metrics`, {
...filters,
type,
limit,
});
options?.onDataLoad?.(data);
return data;
},
enabled: !!websiteId,
...options,
});
}
@@ -1,34 +1,18 @@
import { zonedTimeToUtc } from 'date-fns-tz';
import { useApi, useDateRange, useNavigation, useTimezone } from 'components/hooks';
import { UseQueryOptions } from '@tanstack/react-query';
import { useApi } from './useApi';
import { useFilterParams } from '..//useFilterParams';
export function useWebsitePageviews(websiteId: string, options?: { [key: string]: string }) {
export function useWebsitePageviews(
websiteId: string,
options?: Omit<UseQueryOptions, 'queryKey' | 'queryFn'>,
) {
const { get, useQuery } = useApi();
const [dateRange] = useDateRange(websiteId);
const { startDate, endDate, unit } = dateRange;
const { timezone } = useTimezone();
const {
query: { url, referrer, os, browser, device, country, region, city, title },
} = useNavigation();
const params = {
startAt: +zonedTimeToUtc(startDate, timezone),
endAt: +zonedTimeToUtc(endDate, timezone),
unit,
timezone,
url,
referrer,
os,
browser,
device,
country,
region,
city,
title,
};
const params = useFilterParams(websiteId);
return useQuery({
queryKey: ['websites:pageviews', { websiteId, ...params }],
queryFn: () => get(`/websites/${websiteId}/pageviews`, params),
enabled: !!websiteId,
...options,
});
}
@@ -1,30 +1,14 @@
import { useApi, useDateRange, useNavigation } from 'components/hooks';
import { useApi } from './useApi';
import { useFilterParams } from '../useFilterParams';
export function useWebsiteStats(websiteId: string, options?: { [key: string]: string }) {
const { get, useQuery } = useApi();
const [dateRange] = useDateRange(websiteId);
const { startDate, endDate } = dateRange;
const {
query: { url, referrer, title, os, browser, device, country, region, city },
} = useNavigation();
const params = {
startAt: +startDate,
endAt: +endDate,
url,
referrer,
title,
os,
browser,
device,
country,
region,
city,
};
const params = useFilterParams(websiteId);
return useQuery({
queryKey: ['websites:stats', { websiteId, ...params }],
queryFn: () => get(`/websites/${websiteId}/stats`, params),
enabled: !!websiteId,
...options,
});
}
+32
View File
@@ -0,0 +1,32 @@
import { useNavigation } from './useNavigation';
import { useDateRange } from './useDateRange';
import { useTimezone } from './useTimezone';
import { zonedTimeToUtc } from 'date-fns-tz';
export function useFilterParams(websiteId: string) {
const [dateRange] = useDateRange(websiteId);
const { startDate, endDate, unit, offset } = dateRange;
const { timezone } = useTimezone();
const {
query: { url, referrer, title, query, os, browser, device, country, region, city, event },
} = useNavigation();
return {
startAt: +zonedTimeToUtc(startDate, timezone),
endAt: +zonedTimeToUtc(endDate, timezone),
unit,
offset,
timezone,
url,
referrer,
title,
query,
os,
browser,
device,
country,
region,
city,
event,
};
}
+5 -26
View File
@@ -6,7 +6,6 @@ import LinkButton from 'components/common/LinkButton';
import { DEFAULT_ANIMATION_DURATION } from 'lib/constants';
import { percentFilter } from 'lib/filters';
import {
useDateRange,
useNavigation,
useWebsiteMetrics,
useMessages,
@@ -45,34 +44,14 @@ export function MetricsTable({
}: MetricsTableProps) {
const [search, setSearch] = useState('');
const { formatValue } = useFormat();
const [{ startDate, endDate }] = useDateRange(websiteId);
const {
renderUrl,
query: { url, referrer, title, os, browser, device, country, region, city },
} = useNavigation();
const { renderUrl } = useNavigation();
const { formatMessage, labels } = useMessages();
const { dir } = useLocale();
const { data, isLoading, isFetched, error } = useWebsiteMetrics(
websiteId,
{
type,
startAt: +startDate,
endAt: +endDate,
url,
referrer,
os,
title,
browser,
device,
country,
region,
city,
limit,
search,
},
{ retryDelay: delay || DEFAULT_ANIMATION_DURATION, onDataLoad },
);
const { data, isLoading, isFetched, error } = useWebsiteMetrics(websiteId, type, limit, {
retryDelay: delay || DEFAULT_ANIMATION_DURATION,
onDataLoad,
});
const filteredData = useMemo(() => {
if (data) {
+246
View File
@@ -0,0 +1,246 @@
{
"label.access-code": "Pristupni kod",
"label.actions": "Akcije",
"label.activity-log": "Log aktivnosti",
"label.add": "Dodaj",
"label.add-description": "Dodaj opis",
"label.add-member": "Dodaj člana",
"label.add-step": "Dodaj korak",
"label.add-website": "Dodaj web stranicu",
"label.admin": "Administrator",
"label.after": "Nakon",
"label.all": "Sve",
"label.all-time": "Cijelo vrijeme",
"label.analytics": "Analitike",
"label.average": "Prosjek",
"label.average-visit-time": "Prosječno vrijeme posjete",
"label.back": "Nazad",
"label.before": "Prije",
"label.bounce-rate": "Bounce rate",
"label.breakdown": "Pregled po kategorijama",
"label.browser": "Browser",
"label.browsers": "Browseri",
"label.cancel": "Otkaži",
"label.change-password": "Promijeni šifru",
"label.cities": "Gradovi",
"label.city": "Grad",
"label.clear-all": "Očisti sve",
"label.confirm": "Potvrdi",
"label.confirm-password": "Potvrdi šifru",
"label.contains": "Sadrži",
"label.continue": "Nastavi",
"label.countries": "Zemlje",
"label.country": "Zemlja",
"label.create": "Kreiraj",
"label.create-report": "Kreiraj izvještaj",
"label.create-team": "Kreiraj tim",
"label.create-user": "Kreiraj korisnika",
"label.created": "Kreiraj",
"label.created-by": "Kreirao",
"label.current-password": "Trenutna šifra",
"label.custom-range": "Proizvoljni raspon",
"label.dashboard": "Dashboard",
"label.data": "Podaci",
"label.date": "Datum",
"label.date-range": "Datumski raspon",
"label.day": "Dan",
"label.default-date-range": "Defaultni datumski raspon",
"label.delete": "Izbriši",
"label.delete-report": "Izbriši report",
"label.delete-team": "Izbriši tim",
"label.delete-user": "Izbriši korisnika",
"label.delete-website": "Izbriši web stranicu",
"label.description": "Opis",
"label.desktop": "Desktop",
"label.details": "Detalji",
"label.device": "Uređaj",
"label.devices": "Uređaji",
"label.dismiss": "Odbaci",
"label.does-not-contain": "Ne sadrži",
"label.domain": "Domena",
"label.dropoff": "Dropoff",
"label.edit": "Uredi",
"label.edit-dashboard": "Uredi dashboard",
"label.edit-member": "Uredi člana",
"label.enable-share-url": "Omogući URL za dijeljenje",
"label.event": "Događaj",
"label.event-data": "Podaci o događaju",
"label.events": "Događaji",
"label.false": "Ne",
"label.field": "Polje",
"label.fields": "Polja",
"label.filter": "Filter",
"label.filter-combined": "Kombinovano",
"label.filter-raw": "Sirovo",
"label.filters": "Filtri",
"label.funnel": "Lijevak",
"label.funnel-description": "Razumite koverziju i drop-off učestalost korisnika.",
"label.greater-than": "Veće od",
"label.greater-than-equals": "Veće od ili jednako",
"label.insights": "Uvidi",
"label.insights-description": "Zaronite dublje u vaše podatke korištenjem segmenata i filtera",
"label.is": "Jeste",
"label.is-not": "Nije",
"label.is-not-set": "Nije setano",
"label.is-set": "Jeste setano",
"label.join": "Učlani se",
"label.join-team": "Učlani se u tim",
"label.language": "Jezik",
"label.languages": "Jezici",
"label.laptop": "Laptop",
"label.last-days": "Zadnjih {x} dana",
"label.last-hours": "Zadnjih {x} sati",
"label.last-months": "Zadnjih {x} mjeseci",
"label.leave": "Napusti",
"label.leave-team": "Napusti tim",
"label.less-than": "Manje od",
"label.less-than-equals": "Manje od ili jednako",
"label.login": "Login",
"label.logout": "Logout",
"label.manage": "Manage",
"label.max": "Max",
"label.member": "Član",
"label.members": "Članovi",
"label.min": "Min",
"label.mobile": "Mobile",
"label.more": "Više",
"label.my-account": "Moj račun",
"label.my-websites": "Moje web stranice",
"label.name": "Ime",
"label.new-password": "Nova šifra",
"label.none": "None",
"label.number-of-records": "{x} {x, plural, one {record} other {records}}",
"label.ok": "OK",
"label.os": "OS",
"label.overview": "Pregled",
"label.owner": "Vlasnik",
"label.page-of": "Strana {current} od {total}",
"label.page-views": "Pregleda stranica",
"label.pageTitle": "Naslov stranice",
"label.pages": "Stranice",
"label.password": "Šifra",
"label.powered-by": "Omogućeno s {name}",
"label.profile": "Profil",
"label.queries": "Queryji",
"label.query": "Query",
"label.query-parameters": "Query parametri",
"label.realtime": "Realno vrijeme",
"label.referrer": "Referrer",
"label.referrers": "Referrers",
"label.refresh": "Refresh",
"label.regenerate": "Regeneriši",
"label.region": "Region",
"label.regions": "Regioni",
"label.remove": "Ukloni",
"label.remove-member": "Ukloni člana",
"label.reports": "Izvještaji",
"label.required": "Required",
"label.reset": "Resetuj",
"label.reset-website": "Resetuj web stranicu",
"label.retention": "Retention",
"label.retention-description": "Izmjeri 'ljepljivost' svoje web stranice praćenjem koliko često set korisnici vraćaju.",
"label.role": "Rola",
"label.run-query": "Pokreni query",
"label.save": "Sačuvaj",
"label.screens": "Ekrani",
"label.search": "Traži",
"label.select": "Odaberi",
"label.select-date": "Odaberi datum",
"label.select-role": "Odaberi rolu",
"label.select-website": "Odaberi web stranicu",
"label.sessions": "Sesije",
"label.settings": "Postavke",
"label.share-url": "Share URL",
"label.single-day": "Jedan dan",
"label.steps": "Koraci",
"label.sum": "Suma",
"label.tablet": "Tablet",
"label.team": "Tim",
"label.team-id": "Tim ID",
"label.team-member": "Član tima",
"label.team-name": "Naziv tima",
"label.team-owner": "Vlasnik tima",
"label.team-view-only": "Samo tim može vidjeti",
"label.team-websites": "Timske web stranice",
"label.teams": "Timovi",
"label.theme": "Teme",
"label.this-month": "Ovaj mjesec",
"label.this-week": "Ova sedmica",
"label.this-year": "Ova godina",
"label.timezone": "Vremenska zona",
"label.title": "Naslov",
"label.today": "Danas",
"label.toggle-charts": "Uklj/isklj grafikone",
"label.total": "Ukupno",
"label.total-records": "Ukupno redova",
"label.tracking-code": "Kod za praćenje",
"label.transfer": "Transfer",
"label.transfer-website": "Transfer web stranice",
"label.true": "Da",
"label.type": "Tip",
"label.unique": "Jedinstveno",
"label.unique-visitors": "Jedinstvenih posjetitelja",
"label.unknown": "Nepoznato",
"label.untitled": "Bezimeno",
"label.update": "Update",
"label.url": "URL",
"label.urls": "URLs",
"label.user": "Korisnik",
"label.username": "Korisničko ime",
"label.users": "Korisnici",
"label.utm": "UTM",
"label.utm-description": "Pratite vaše kampanje kroz UTM parametre.",
"label.value": "Vrijednost",
"label.view": "Pregled",
"label.view-details": "Pogledaj detalje",
"label.view-only": "Samo gledanje",
"label.views": "Pregledi",
"label.views-per-visit": "Pregledi po posjeti",
"label.visitors": "Posjetitelji",
"label.visits": "Posjete",
"label.website": "Web stranica",
"label.website-id": "ID web stranice",
"label.websites": "Web stranice",
"label.window": "Prozor",
"label.yesterday": "Jučer",
"message.action-confirmation": "Unesite {confirmation} ispod da potvrdite.",
"message.active-users": "{x} trenutno {x, plural, one {posjetitelj} other {posjetitelja}}",
"message.confirm-delete": "Jeste li sigurni da želite obrisati {target}?",
"message.confirm-leave": "Jeste li sigurni da želite napustiti {target}?",
"message.confirm-remove": "Jeste li sigurni da želite ukloniti {target}?",
"message.confirm-reset": "Jeste li sigurni da želite resetovati {target}?",
"message.delete-team-warning": "Brisanje tima će također obrisati sve web stranice tima.",
"message.delete-website-warning": "Svi podaci web stranice biće obrisani.",
"message.error": "Nešto je pošlo po zlu.",
"message.event-log": "{event} na {url}",
"message.go-to-settings": "Idi na postavke",
"message.incorrect-username-password": "Pogrešno korisničko ime i/ili šifra.",
"message.invalid-domain": "Nevalidna domena. Ne uključujte http/https.",
"message.min-password-length": "Minimalna dužina od {n} karaktera",
"message.new-version-available": "Nova verzija Umami {version} je dostupna!",
"message.no-data-available": "Nema dostupnih podataka.",
"message.no-event-data": "Nema dostupnih podataka o događajima.",
"message.no-match-password": "Šifre se ne poklapaju.",
"message.no-results-found": "Nema rezultata.",
"message.no-team-websites": "Ovaj tim nema nikakvih web stranica.",
"message.no-teams": "Niste kreirali nijedan tim.",
"message.no-users": "Nema nikakvih korisnika.",
"message.no-websites-configured": "Nemate iskonfigurisanu nijednu web stranicu.",
"message.page-not-found": "Stranica nije pronađena",
"message.reset-website": "Da resetujete ovu web stranicu, upišite {confirmation} dole da potvrdite.",
"message.reset-website-warning": "Sve statistike o ovoj web stranici će biti obrisane, ali vaše postavke neće biti dirane.",
"message.saved": "Sačuvano.",
"message.share-url": "Statistike vaše web stranice su javno dostupne na sljedećem URLu:",
"message.team-already-member": "Već ste član tima.",
"message.team-not-found": "Tim nije pronađen.",
"message.team-websites-info": "Web stranice može vidjeti bilo ko iz tima.",
"message.tracking-code": "Da pratite statistike ove web stranice, stavite sljedeći kod u <head>...</head> sekciju vašeg HTMLa.",
"message.transfer-team-website-to-user": "Prebacite ovu web stranicu na vaš račun?",
"message.transfer-user-website-to-team": "Odaberite tim u koji želite prebaciti ovu web stranicu.",
"message.transfer-website": "Prebacite vlasništvo web stranice na vaš račun ili drugi tim.",
"message.triggered-event": "Trigerovani događaj",
"message.user-deleted": "Korisnik obrisan.",
"message.viewed-page": "Pogledana stranica",
"message.visitor-log": "Posjetitelj iz {country} koristi {browser} na {os} {device}",
"message.visitors-dropped-off": "Posjetitelji koji su napustili stranicu"
}
+150 -150
View File
@@ -1,246 +1,246 @@
{
"label.access-code": "Access code",
"label.access-code": "Codi d'accés",
"label.actions": "Accions",
"label.activity-log": "Activity log",
"label.add": "Add",
"label.add-description": "Add description",
"label.add-member": "Add member",
"label.add-step": "Add step",
"label.add-website": "Afegeix lloc web",
"label.activity-log": "Registre d'activitat",
"label.add": "Afegir",
"label.add-description": "Afegir descripció",
"label.add-member": "Afegir membre",
"label.add-step": "Afegir pas",
"label.add-website": "Afegir lloc web",
"label.admin": "Administrador",
"label.after": "After",
"label.after": "Després",
"label.all": "Tots",
"label.all-time": "Sempre",
"label.analytics": "Analytics",
"label.average": "Average",
"label.analytics": "Analítiques",
"label.average": "Mitjana",
"label.average-visit-time": "Temps mitjà de visita",
"label.back": "Enrere",
"label.before": "Before",
"label.before": "Abans",
"label.bounce-rate": "Percentatge de rebot",
"label.breakdown": "Breakdown",
"label.browser": "Browser",
"label.breakdown": "Desglossament",
"label.browser": "Navegador",
"label.browsers": "Navegadors",
"label.cancel": "Cancel·la",
"label.change-password": "Canvia la contrasenya",
"label.cities": "Cities",
"label.city": "City",
"label.clear-all": "Clear all",
"label.confirm": "Confirm",
"label.cities": "Ciutats",
"label.city": "Ciutat",
"label.clear-all": "Netejar tot",
"label.confirm": "Confirmar",
"label.confirm-password": "Confirma la contrasenya",
"label.contains": "Contains",
"label.continue": "Continue",
"label.contains": "Conté",
"label.continue": "Continuar",
"label.countries": "Països",
"label.country": "Country",
"label.create": "Create",
"label.create-report": "Create report",
"label.create-team": "Create team",
"label.create-user": "Create user",
"label.created": "Created",
"label.created-by": "Created By",
"label.country": "País",
"label.create": "Crear",
"label.create-report": "Crear informe",
"label.create-team": "Crear equip",
"label.create-user": "Crear usuari",
"label.created": "Creat",
"label.created-by": "Creat Per",
"label.current-password": "Contrasenya actual",
"label.custom-range": "Rang personalitzat",
"label.dashboard": "Panell",
"label.data": "Data",
"label.date": "Date",
"label.data": "Dades",
"label.date": "Data",
"label.date-range": "Interval de dates",
"label.day": "Day",
"label.day": "Dia",
"label.default-date-range": "Interval de dates per defecte",
"label.delete": "Esborra",
"label.delete-report": "Delete report",
"label.delete-team": "Delete team",
"label.delete-user": "Delete user",
"label.delete-report": "Eliminar informe",
"label.delete-team": "Eliminar equip",
"label.delete-user": "Eliminar usuari",
"label.delete-website": "Esborra el lloc web",
"label.description": "Description",
"label.description": "Descripció",
"label.desktop": "Escriptori",
"label.details": "Details",
"label.device": "Device",
"label.details": "Detalls",
"label.device": "Dispositiu",
"label.devices": "Dispositius",
"label.dismiss": "Descarta",
"label.does-not-contain": "Does not contain",
"label.does-not-contain": "No conté",
"label.domain": "Domini",
"label.dropoff": "Dropoff",
"label.dropoff": "Abandonament",
"label.edit": "Edita",
"label.edit-dashboard": "Edit dashboard",
"label.edit-member": "Edit member",
"label.edit-dashboard": "Edita panell",
"label.edit-member": "Edita membre",
"label.enable-share-url": "Activa l'enllaç per compartir",
"label.event": "Event",
"label.event-data": "Event data",
"label.event": "Esdeveniment",
"label.event-data": "Dades de l'esdeveniment",
"label.events": "Esdeveniments",
"label.false": "False",
"label.field": "Field",
"label.fields": "Fields",
"label.filter": "Filter",
"label.false": "Fals",
"label.field": "Camp",
"label.fields": "Camps",
"label.filter": "Filtre",
"label.filter-combined": "Combinat",
"label.filter-raw": "En cru",
"label.filters": "Filters",
"label.funnel": "Funnel",
"label.funnel-description": "Understand the conversion and drop-off rate of users.",
"label.greater-than": "Greater than",
"label.greater-than-equals": "Greater than or equals",
"label.filters": "Filtres",
"label.funnel": "Embut",
"label.funnel-description": "Entengui la taxa de conversió i abandonament dels usuaris.",
"label.greater-than": "Més gran que",
"label.greater-than-equals": "Més gran que o igual a",
"label.insights": "Insights",
"label.insights-description": "Dive deeper into your data by using segments and filters.",
"label.is": "Is",
"label.is-not": "Is not",
"label.is-not-set": "Is not set",
"label.is-set": "Is set",
"label.join": "Join",
"label.join-team": "Join team",
"label.language": "Language",
"label.languages": "Llengües",
"label.insights-description": "Aprofundeixi en les seves dades mitjançant l'ús de segments i filtres.",
"label.is": "És igual a",
"label.is-not": "No és igual a",
"label.is-not-set": "No està establert",
"label.is-set": "Està establert",
"label.join": "Unir",
"label.join-team": "Unir-se al equip",
"label.language": "Idioma",
"label.languages": "Idiomes",
"label.laptop": "Portàtil",
"label.last-days": "Últims {x} dies",
"label.last-hours": "Últimes {x} hores",
"label.last-months": "Last {x} months",
"label.leave": "Leave",
"label.leave-team": "Leave team",
"label.less-than": "Less than",
"label.less-than-equals": "Less than or equals",
"label.last-months": "Últims {x} mesos",
"label.leave": "Abandonar",
"label.leave-team": "Abandonar equip",
"label.less-than": "Menor que",
"label.less-than-equals": "Menor que o igual a",
"label.login": "Connecta't",
"label.logout": "Desconnecta't",
"label.manage": "Manage",
"label.max": "Max",
"label.member": "Member",
"label.members": "Members",
"label.min": "Min",
"label.manage": "Administrar",
"label.max": "Màx",
"label.member": "Membre",
"label.members": "Membres",
"label.min": "Mín",
"label.mobile": "Mòbil",
"label.more": "Més",
"label.my-account": "My account",
"label.my-websites": "My websites",
"label.my-account": "El meu compte",
"label.my-websites": "Els meus llocs web",
"label.name": "Nom",
"label.new-password": "Contrasenya nova",
"label.none": "None",
"label.none": "Cap",
"label.number-of-records": "{x} {x, plural, one {record} other {records}}",
"label.ok": "OK",
"label.os": "OS",
"label.overview": "Overview",
"label.os": "SO",
"label.overview": "Resum",
"label.owner": "Propietari",
"label.page-of": "Page {current} of {total}",
"label.page-of": "Pàgina {current} de {total}",
"label.page-views": "Pàgines vistes",
"label.pageTitle": "Page title",
"label.pageTitle": "Títol de la pàgina",
"label.pages": "Pàgines",
"label.password": "Contrasenya",
"label.powered-by": "Funciona amb {name}",
"label.profile": "Perfil",
"label.queries": "Queries",
"label.query": "Query",
"label.query-parameters": "Query parameters",
"label.queries": "Consultes",
"label.query": "Consulta",
"label.query-parameters": "Paràmetres de consulta",
"label.realtime": "Temps real",
"label.referrer": "Referrer",
"label.referrer": "Referent",
"label.referrers": "Referents",
"label.refresh": "Refresca",
"label.regenerate": "Regenerate",
"label.region": "Region",
"label.regenerate": "Regenerar",
"label.region": "Regió",
"label.regions": "Regions",
"label.remove": "Remove",
"label.remove-member": "Remove member",
"label.reports": "Reports",
"label.remove": "Treure",
"label.remove-member": "Eliminar membre",
"label.reports": "Informes",
"label.required": "Obligatori",
"label.reset": "Restableix",
"label.reset-website": "Restableix estadístiques",
"label.retention": "Retention",
"label.retention-description": "Measure your website stickiness by tracking how often users return.",
"label.role": "Role",
"label.run-query": "Run query",
"label.retention": "Retenció",
"label.retention-description": "Mesuri la retenció del seu lloc web fent un seguiment de la freqüència amb què tornen els usuaris.",
"label.role": "Rol",
"label.run-query": "Executar consulta",
"label.save": "Desa",
"label.screens": "Screens",
"label.search": "Search",
"label.select": "Select",
"label.select-date": "Select date",
"label.select-role": "Select role",
"label.select-website": "Select website",
"label.screens": "Pantalles",
"label.search": "Buscar",
"label.select": "Seleccionar",
"label.select-date": "Seleccionar data",
"label.select-role": "Seleccionar rol",
"label.select-website": "Seleccionar lloc web",
"label.sessions": "Sessions",
"label.settings": "Configuració",
"label.share-url": "Enllaç per compartir",
"label.single-day": "Un sol dia",
"label.steps": "Steps",
"label.sum": "Sum",
"label.steps": "Pasos",
"label.sum": "Suma",
"label.tablet": "Tauleta",
"label.team": "Team",
"label.team-id": "Team ID",
"label.team-member": "Team member",
"label.team-name": "Team name",
"label.team-owner": "Team owner",
"label.team-view-only": "Team view only",
"label.team-websites": "Team websites",
"label.teams": "Teams",
"label.theme": "Theme",
"label.team": "Equip",
"label.team-id": "ID del equip",
"label.team-member": "Membre de l'equip",
"label.team-name": "Nom de l'equip",
"label.team-owner": "Propietari de l'equip",
"label.team-view-only": "Vista només de l'equip",
"label.team-websites": "Llocs web de l'equip",
"label.teams": "Equips",
"label.theme": "Tema",
"label.this-month": "Aquest mes",
"label.this-week": "Aquesta setmana",
"label.this-year": "Aquest any",
"label.timezone": "Zona horària",
"label.title": "Title",
"label.title": "Títol",
"label.today": "Avui",
"label.toggle-charts": "Mostra/amaga gràfics",
"label.total": "Total",
"label.total-records": "Total records",
"label.total-records": "Total de registres",
"label.tracking-code": "Codi de seguiment",
"label.transfer": "Transfer",
"label.transfer-website": "Transfer website",
"label.true": "True",
"label.type": "Type",
"label.unique": "Unique",
"label.transfer": "Transferir",
"label.transfer-website": "Transferir lloc web",
"label.true": "Cert",
"label.type": "Tipus",
"label.unique": "Únic",
"label.unique-visitors": "Visitants únics",
"label.unknown": "Desconegut",
"label.untitled": "Untitled",
"label.update": "Update",
"label.untitled": "Sense títol",
"label.update": "Actualitzar",
"label.url": "URL",
"label.urls": "URLs",
"label.user": "User",
"label.user": "Usuari",
"label.username": "Nom d'usuari",
"label.users": "Users",
"label.users": "Usuaris",
"label.utm": "UTM",
"label.utm-description": "Track your campaigns through UTM parameters.",
"label.value": "Value",
"label.view": "View",
"label.utm-description": "Rastreji les seves campanyes a través de paràmetres UTM.",
"label.value": "Valor",
"label.view": "Visualitzar",
"label.view-details": "Veure els detalls",
"label.view-only": "View only",
"label.view-only": "Només veure",
"label.views": "Vistes",
"label.views-per-visit": "Views per visit",
"label.visitors": "Visitants",
"label.visits": "Visits",
"label.website": "Website",
"label.website-id": "Website ID",
"label.visits": "Visites",
"label.website": "Lloc web",
"label.website-id": "ID del lloc web",
"label.websites": "Llocs web",
"label.window": "Window",
"label.window": "Finestra",
"label.yesterday": "Ahir",
"message.action-confirmation": "Type {confirmation} in the box below to confirm.",
"message.action-confirmation": "Escrigui {confirmation} al cuadre inferior per confirmar.",
"message.active-users": "{x} {x, plural, one {visitant actual} other {visitants actuals}}",
"message.confirm-delete": "Segur que vols esborrar {target}?",
"message.confirm-leave": "Are you sure you want to leave {target}?",
"message.confirm-remove": "Are you sure you want to remove {target}?",
"message.confirm-reset": "Segur que vols restablir les estadístiques de {target}?",
"message.delete-team-warning": "Deleting a team will also delete all team websites.",
"message.confirm-delete": "Segur que vol esborrar {target}?",
"message.confirm-leave": "Segur que vol abandonar {target}?",
"message.confirm-remove": "Segur que vol eliminar {target}?",
"message.confirm-reset": "Segur que vol restablir les estadístiques de {target}?",
"message.delete-team-warning": "Al eliminar un equip també s'eliminaran tots els llocs web de l'equip.",
"message.delete-website-warning": "També s'esborraran totes les dades relacionades.",
"message.error": "S'ha produït un error.",
"message.event-log": "{event} on {url}",
"message.event-log": "{event} a {url}",
"message.go-to-settings": "Vés a la configuració",
"message.incorrect-username-password": "Nom d'usuari o contrasenya incorrectes.",
"message.invalid-domain": "Domini invàlid",
"message.min-password-length": "Minimum length of {n} characters",
"message.new-version-available": "A new version of Umami {version} is available!",
"message.min-password-length": "Longitud mínima de {n} caràcters",
"message.new-version-available": "Una nova versió d'Umami {version} està disponible!",
"message.no-data-available": "No hi ha dades disponibles.",
"message.no-event-data": "No event data is available.",
"message.no-event-data": "No hi ha dades d'esdeveniments disponibles.",
"message.no-match-password": "Les contrasenyes no coincideixen",
"message.no-results-found": "No results were found.",
"message.no-team-websites": "This team does not have any websites.",
"message.no-teams": "You have not created any teams.",
"message.no-users": "There are no users.",
"message.no-results-found": "No s'han trobat resultats.",
"message.no-team-websites": "Aquest equip no té cap lloc web.",
"message.no-teams": "No ha creat cap equip.",
"message.no-users": "No hi ha cap usuari.",
"message.no-websites-configured": "No hi ha cap lloc web configurat.",
"message.page-not-found": "No s'ha trobat la pàgina.",
"message.reset-website": "To reset this website, type {confirmation} in the box below to confirm.",
"message.reset-website": "Per restablir aquest lloc web, escrigui {confirmation} al cuadre inferior per confirmar.",
"message.reset-website-warning": "S'esborraran totes les estadístiques per aquest lloc web, però el codi de seguiment es mantindrà.",
"message.saved": "S'ha desat amb èxit.",
"message.share-url": "Aquest és l'enllaç públic per compartir de {target}.",
"message.team-already-member": "You are already a member of the team.",
"message.team-not-found": "Team not found.",
"message.team-websites-info": "Websites can be viewed by anyone on the team.",
"message.team-already-member": "Ja és membre d'aquest equip.",
"message.team-not-found": "Equip no trobat.",
"message.team-websites-info": "Els llocs web poden ser visualitzats per qualsevol membre de l'equip.",
"message.tracking-code": "Codi de seguiment",
"message.transfer-team-website-to-user": "Transfer this website to your account?",
"message.transfer-user-website-to-team": "Select the team to transfer this website to.",
"message.transfer-website": "Transfer website ownership to your account or another team.",
"message.triggered-event": "Triggered event",
"message.user-deleted": "User deleted.",
"message.viewed-page": "Viewed page",
"message.transfer-team-website-to-user": "Transferir aquest lloc web al seu compte?",
"message.transfer-user-website-to-team": "Seleccioni l'equip al qui transferir aquest lloc web.",
"message.transfer-website": "Transferir la propietat del lloc web al seu compte o a un altre equip.",
"message.triggered-event": "Esdeveniment desencadenat",
"message.user-deleted": "Usuari eliminat.",
"message.viewed-page": "Pàgina vista",
"message.visitor-log": "Visitant de {country} usant {browser} a {os} {device}",
"message.visitors-dropped-off": "Visitors dropped off"
"message.visitors-dropped-off": "Els visitants han sortit"
}
+154 -154
View File
@@ -1,246 +1,246 @@
{
"label.access-code": "Access code",
"label.access-code": "Erişim Kodu",
"label.actions": "Hareketler",
"label.activity-log": "Activity log",
"label.add": "Add",
"label.add-description": "Add description",
"label.add-member": "Add member",
"label.add-step": "Add step",
"label.activity-log": "Aktivite Kaydı",
"label.add": "Ekle",
"label.add-description": "Açıklama ekle",
"label.add-member": "Üye ekle",
"label.add-step": "Adım ekle",
"label.add-website": "Web sitesi ekle",
"label.admin": "Yönetici",
"label.after": "After",
"label.administrator": "Yönetici",
"label.after": "Sonra",
"label.all": "Tümü",
"label.all-time": "All time",
"label.analytics": "Analytics",
"label.average": "Average",
"label.all-time": "Tüm zamanlar",
"label.analytics": "Analitik",
"label.average": "Ortalama",
"label.average-visit-time": "Ortalama ziyaret süresi",
"label.back": "Geri",
"label.before": "Before",
"label.bounce-rate": "Çıkma oranı",
"label.breakdown": "Breakdown",
"label.browser": "Browser",
"label.before": "Önce",
"label.bounce-rate": "Tek sayfa ziyaret oranı",
"label.breakdown": "Dağılım",
"label.browser": "Tarayıcı",
"label.browsers": "Tarayıcılar",
"label.cancel": "İptal",
"label.change-password": "Şifre değiştir",
"label.cities": "Cities",
"label.city": "City",
"label.clear-all": "Clear all",
"label.confirm": "Confirm",
"label.cities": "Şehirler",
"label.city": "Şehir",
"label.clear-all": "Hepsini temizle",
"label.confirm": "Onayla",
"label.confirm-password": "Parolayı onayla",
"label.contains": "Contains",
"label.continue": "Continue",
"label.contains": "İçeriği",
"label.continue": "Devam et",
"label.countries": "Ülkeler",
"label.country": "Country",
"label.create": "Create",
"label.create-report": "Create report",
"label.create-team": "Create team",
"label.create-user": "Create user",
"label.created": "Created",
"label.created-by": "Created By",
"label.country": "Ülke",
"label.create": "Oluştur",
"label.create-report": "Rapor oluştur",
"label.create-team": "Takım oluştur",
"label.create-user": "Kullanıcı oluştur",
"label.created": "Oluşturuldu",
"label.created-by": "Tarafından oluşturldu",
"label.current-password": "Mevcut parola",
"label.custom-range": "Özelleştirilmiş aralık",
"label.dashboard": "Kontrol Paneli",
"label.data": "Data",
"label.date": "Date",
"label.data": "Veri",
"label.date": "Tarih",
"label.date-range": "Tarih aralığı",
"label.day": "Day",
"label.day": "Gün",
"label.default-date-range": "Varsayılan tarih aralığı",
"label.delete": "Sil",
"label.delete-report": "Delete report",
"label.delete-team": "Delete team",
"label.delete-user": "Delete user",
"label.delete-report": "Rapor sil",
"label.delete-team": "Takım sil",
"label.delete-user": "Kullanıcı sil",
"label.delete-website": "Web sitesini sil",
"label.description": "Description",
"label.description": "Açıklama",
"label.desktop": "Masaüstü",
"label.details": "Details",
"label.device": "Device",
"label.details": "Detaylar",
"label.device": "Cihaz",
"label.devices": "Cihazlar",
"label.dismiss": "Reddet",
"label.does-not-contain": "Does not contain",
"label.does-not-contain": "İçermez",
"label.domain": "Alan adı",
"label.dropoff": "Dropoff",
"label.dropoff": "Bırakma",
"label.edit": "Düzenle",
"label.edit-dashboard": "Edit dashboard",
"label.edit-member": "Edit member",
"label.edit-dashboard": "Kontrol panelini düzenle",
"label.edit-member": "Üyeyi düzenle",
"label.enable-share-url": "Anonim paylaşım URL'i aktif",
"label.event": "Event",
"label.event-data": "Event data",
"label.event": "Olay",
"label.event-data": "Olay verisi",
"label.events": "Olaylar",
"label.false": "False",
"label.field": "Field",
"label.fields": "Fields",
"label.filter": "Filter",
"label.filter-combined": "Birleşik",
"label.filter-raw": "Ham",
"label.filters": "Filters",
"label.funnel": "Funnel",
"label.funnel-description": "Understand the conversion and drop-off rate of users.",
"label.greater-than": "Greater than",
"label.greater-than-equals": "Greater than or equals",
"label.false": "Yanlış",
"label.field": "Alan",
"label.fields": "Alanlar",
"label.filter": "Filtre",
"label.filter-combined": "Birleşik filtre",
"label.filter-raw": "Ham filtre",
"label.filters": "Filtreler",
"label.funnel": "Huni",
"label.funnel-description": "Kullanıcıların dönüşüm ve ayrılma oranlarını anlayın.",
"label.greater-than": "Büyüktür",
"label.greater-than-equals": "Büyük veya eşittir",
"label.insights": "Insights",
"label.insights-description": "Dive deeper into your data by using segments and filters.",
"label.insights-description": "Segmentleri ve filtreleri kullanarak verilerinizi derinlemesine inceleyin.",
"label.is": "Is",
"label.is-not": "Is not",
"label.is-not-set": "Is not set",
"label.is-set": "Is set",
"label.join": "Join",
"label.join-team": "Join team",
"label.language": "Language",
"label.languages": "Languages",
"label.is-not": "Değil",
"label.is-not-set": "Ayarlanmamış",
"label.is-set": "Ayarlandı",
"label.join": "Katıl",
"label.join-team": "Takıma katıl",
"label.language": "Dil",
"label.languages": "Diller",
"label.laptop": "Dizüstü",
"label.last-days": "Son {x} gün",
"label.last-hours": "Son {x} saat",
"label.last-months": "Last {x} months",
"label.leave": "Leave",
"label.leave-team": "Leave team",
"label.less-than": "Less than",
"label.less-than-equals": "Less than or equals",
"label.last-months": "Son {x} ay",
"label.leave": "Ayrıl",
"label.leave-team": "Takımdan Ayrıl",
"label.less-than": "Küçüktür",
"label.less-than-equals": "Küçük veya eşittir",
"label.login": "Giriş Yap",
"label.logout": "Çıkış Yap",
"label.manage": "Manage",
"label.manage": "Yönet",
"label.max": "Max",
"label.member": "Member",
"label.members": "Members",
"label.member": "Üye",
"label.members": "Üyeler",
"label.min": "Min",
"label.mobile": "Mobil Cihaz",
"label.more": "Detaylı göster",
"label.my-account": "My account",
"label.my-websites": "My websites",
"label.my-account": "Hesabım",
"label.my-websites": "Web sitelerim",
"label.name": "İsim",
"label.new-password": "Yeni parola",
"label.none": "None",
"label.none": "Yok",
"label.number-of-records": "{x} {x, plural, one {record} other {records}}",
"label.ok": "OK",
"label.ok": "TAMAM",
"label.os": "OS",
"label.overview": "Overview",
"label.owner": "Owner",
"label.page-of": "Page {current} of {total}",
"label.overview": "Genel bakış",
"label.owner": "Sahibi",
"label.page-of": "{total} sayfada {current} ",
"label.page-views": "Sayfa görünümü",
"label.pageTitle": "Page title",
"label.pageTitle": "Sayfa başlığı",
"label.pages": "Sayfalar",
"label.password": "Parola",
"label.powered-by": "Sağlayıcı: {name}",
"label.profile": "Profil",
"label.queries": "Queries",
"label.query": "Query",
"label.query-parameters": "Query parameters",
"label.queries": "Sorgular",
"label.query": "Sorgu",
"label.query-parameters": "Sorgu parametreleri",
"label.realtime": "Gerçek Zamanlı",
"label.referrer": "Referrer",
"label.referrers": "Yönlendirenler",
"label.refresh": "Yenile",
"label.regenerate": "Regenerate",
"label.region": "Region",
"label.regions": "Regions",
"label.remove": "Remove",
"label.remove-member": "Remove member",
"label.reports": "Reports",
"label.regenerate": "Yeniden Oluştur",
"label.region": "Bölge",
"label.regions": "Bölgeler",
"label.remove": "Kaldır",
"label.remove-member": "Üyeyi kaldır",
"label.reports": "Raporlar",
"label.required": "Zorunlu alan",
"label.reset": "Sıfırla",
"label.reset-website": "Reset statistics",
"label.retention": "Retention",
"label.retention-description": "Measure your website stickiness by tracking how often users return.",
"label.role": "Role",
"label.run-query": "Run query",
"label.reset-website": "İstatistikleri sıfırla",
"label.retention": "Geri dönüş",
"label.retention-description": "Kullanıcıların ne sıklıkla geri döndüğünü takip ederek web sitenizin kalıcılığını ölçün.",
"label.role": "Rol",
"label.run-query": "Sorgu çalıştır",
"label.save": "Kaydet",
"label.screens": "Ekranlar",
"label.search": "Search",
"label.select": "Select",
"label.select-date": "Select date",
"label.select-role": "Select role",
"label.select-website": "Select website",
"label.search": "Ara",
"label.select": "Seç",
"label.select-date": "Tarih seç",
"label.select-role": "Rol seç",
"label.select-website": "Web sitesi seç",
"label.sessions": "Sessions",
"label.settings": "Ayarlar",
"label.share-url": "Paylaşım adresi",
"label.single-day": "Tekil gün",
"label.steps": "Steps",
"label.sum": "Sum",
"label.steps": "Adımlar",
"label.sum": "Toplam",
"label.tablet": "Tablet",
"label.team": "Team",
"label.team-id": "Team ID",
"label.team-member": "Team member",
"label.team-name": "Team name",
"label.team-owner": "Team owner",
"label.team-view-only": "Team view only",
"label.team-websites": "Team websites",
"label.teams": "Teams",
"label.theme": "Theme",
"label.team": "Takım",
"label.team-id": "Takım ID",
"label.team-member": "Takım üyesi",
"label.team-name": "Takım ismi",
"label.team-owner": "Takım sahibi",
"label.team-view-only": "Yalnızca ekip görünümü",
"label.team-websites": "Takım web siteleri",
"label.teams": "Takımlar",
"label.theme": "Tema",
"label.this-month": "Bu ay",
"label.this-week": "Bu hafta",
"label.this-year": "Bu yıl",
"label.timezone": "Zaman dilimi",
"label.title": "Title",
"label.title": "Başlık",
"label.today": "Bugün",
"label.toggle-charts": "Toggle charts",
"label.total": "Total",
"label.total-records": "Total records",
"label.toggle-charts": "Grafikleri değiştir",
"label.total": "Toplam",
"label.total-records": "Toplam kayıt",
"label.tracking-code": "İzleme kodu",
"label.transfer": "Transfer",
"label.transfer-website": "Transfer website",
"label.true": "True",
"label.type": "Type",
"label.unique": "Unique",
"label.transfer-website": "Transfer web sitesi",
"label.true": "Doğru",
"label.type": "Tip",
"label.unique": "Benzersiz",
"label.unique-visitors": "Tekil kullanıcı",
"label.unknown": "Bilinmeyen",
"label.untitled": "Untitled",
"label.update": "Update",
"label.untitled": "İsimsiz",
"label.update": "Güncelle",
"label.url": "URL",
"label.urls": "URLs",
"label.user": "User",
"label.user": "Kullanıcı",
"label.username": "Kullanıcı adı",
"label.users": "Users",
"label.users": "Kullanıcılar",
"label.utm": "UTM",
"label.utm-description": "Track your campaigns through UTM parameters.",
"label.value": "Value",
"label.view": "View",
"label.utm-description": "Kampanyalarınızı UTM parametreleri aracılığıyla takip edin.",
"label.value": "Değer",
"label.view": "Görünüm",
"label.view-details": "Detayı incele",
"label.view-only": "View only",
"label.view-only": "Sadece görünüm",
"label.views": "Görüntüleme",
"label.views-per-visit": "Views per visit",
"label.views-per-visit": "Ziyaret başına görüntüleme",
"label.visitors": "Ziyaretçi",
"label.visits": "Visits",
"label.website": "Website",
"label.visits": "Ziyaretler",
"label.website": "Web sitesi",
"label.website-id": "Website ID",
"label.websites": "Web siteleri",
"label.window": "Window",
"label.yesterday": "Yesterday",
"message.action-confirmation": "Type {confirmation} in the box below to confirm.",
"label.window": "Pencere",
"label.yesterday": "Dün",
"message.action-confirmation": "Onaylamak için aşağıdaki kutuya {confirmation} yazın.",
"message.active-users": "{x} aktif ziyaretçi",
"message.confirm-delete": "{target} kaydını silmek istediğinizden emin misiniz?",
"message.confirm-leave": "Are you sure you want to leave {target}?",
"message.confirm-remove": "Are you sure you want to remove {target}?",
"message.confirm-reset": "Are your sure you want to reset {target}'s statistics?",
"message.delete-team-warning": "Deleting a team will also delete all team websites.",
"message.delete-website-warning": "İlişkili tüm veriler de silinecektir.",
"message.confirm-leave": "{target} kaydından ayrılmak istediğinizden emin misiniz?",
"message.confirm-remove": "{target} kaydını kaldırmak istediğinizden emin misiniz?",
"message.confirm-reset": "{target} istatistiklerini sıfırlamak istediğinizden emin misiniz?",
"message.delete-team-warning": "Bir takımı silmek tüm takım web sitelerini de silecektir.",
"message.delete-website-warning": "İlişkili tüm veriler de silinecektir.",
"message.error": "Bir şeyler ters gitti!",
"message.event-log": "{event} on {url}",
"message.go-to-settings": "Ayarlara git",
"message.incorrect-username-password": "Hatalı kullanıcı adı ya da parola.",
"message.invalid-domain": "Geçersiz alan adı",
"message.min-password-length": "Minimum length of {n} characters",
"message.new-version-available": "A new version of Umami {version} is available!",
"message.min-password-length": "Minimum {n} karakter uzunluğu",
"message.new-version-available": "Yeni versiyon Umami {version} mevcut!",
"message.no-data-available": "Henüz hiç veri yok.",
"message.no-event-data": "No event data is available.",
"message.no-event-data": "Hiçbir olay verisi mevcut değil.",
"message.no-match-password": "Parolalar uyuşmuyor",
"message.no-results-found": "No results were found.",
"message.no-team-websites": "This team does not have any websites.",
"message.no-teams": "You have not created any teams.",
"message.no-users": "There are no users.",
"message.no-results-found": "Hiçbir sonuç bulunamadı.",
"message.no-team-websites": "Bu takımın herhangi bir web sitesi yok.",
"message.no-teams": "Herhangi bir takım oluşturmadınız.",
"message.no-users": "Kullanıcı yok.",
"message.no-websites-configured": "Henüz hiç web sitesi tanımlamadınız",
"message.page-not-found": "Sayfa bulunamadı.",
"message.reset-website": "To reset this website, type {confirmation} in the box below to confirm.",
"message.reset-website-warning": "All statistics for this website will be deleted, but your tracking code will remain intact.",
"message.reset-website": "Bu websitesini sıfılamak için aşağıdaki kutuya {confirmation} yazın.",
"message.reset-website-warning": "Bu web sitesi için tüm istatistikler silinecek, ancak izleme kodunuz bozulmadan kalacaktır.",
"message.saved": "Başarıyla kaydedildi.",
"message.share-url": "{target} için kullanılabilir anonim paylaşım adresidir.",
"message.team-already-member": "You are already a member of the team.",
"message.team-not-found": "Team not found.",
"message.team-websites-info": "Websites can be viewed by anyone on the team.",
"message.team-already-member": "Zaten bu takımın üyesisiniz",
"message.team-not-found": "Takım bulunamadı",
"message.team-websites-info": "Web siteleri takımdaki herkes tarafından görüntülenebilir.",
"message.tracking-code": "İzleme kodu",
"message.transfer-team-website-to-user": "Transfer this website to your account?",
"message.transfer-user-website-to-team": "Select the team to transfer this website to.",
"message.transfer-website": "Transfer website ownership to your account or another team.",
"message.triggered-event": "Triggered event",
"message.user-deleted": "User deleted.",
"message.viewed-page": "Viewed page",
"message.transfer-team-website-to-user": "Bu web sitesi hesbınıza aktarılsın mı?",
"message.transfer-user-website-to-team": "Bu web sitesinin aktarılacağı takımı seçin.",
"message.transfer-website": "Web sitesi sahipliğini hesabınıza veya başka bir takıma aktarın",
"message.triggered-event": "Tetiklenen olay",
"message.user-deleted": "Kullanıcı silindi.",
"message.viewed-page": "Görüntülenen sayfa",
"message.visitor-log": "Yeni ziyaretçi: {country}, {os}, {device}, {browser}",
"message.visitors-dropped-off": "Visitors dropped off"
"message.visitors-dropped-off": "Bırakan ziyaretçiler"
}
+7 -7
View File
@@ -5,7 +5,7 @@
"label.add": "添加",
"label.add-description": "添加描述",
"label.add-member": "添加成员",
"label.add-step": "Add step",
"label.add-step": "添加步骤",
"label.add-website": "添加网站",
"label.admin": "管理员",
"label.after": "之后",
@@ -90,7 +90,7 @@
"label.laptop": "笔记本",
"label.last-days": "最近 {x} 天",
"label.last-hours": "最近 {x} 小时",
"label.last-months": "Last {x} months",
"label.last-months": "最近 {x} 个月",
"label.leave": "离开",
"label.leave-team": "离开团队",
"label.less-than": "少于",
@@ -152,7 +152,7 @@
"label.settings": "设置",
"label.share-url": "共享链接",
"label.single-day": "单日",
"label.steps": "Steps",
"label.steps": "步骤",
"label.sum": "总和",
"label.tablet": "平板",
"label.team": "团队",
@@ -182,22 +182,22 @@
"label.unique-visitors": "独立访客",
"label.unknown": "未知",
"label.untitled": "未命名",
"label.update": "Update",
"label.update": "更新",
"label.url": "网址",
"label.urls": "网址",
"label.user": "用户",
"label.username": "用户名",
"label.users": "用户",
"label.utm": "UTM",
"label.utm-description": "Track your campaigns through UTM parameters.",
"label.utm-description": "通过UTM参数追踪您的广告活动。",
"label.value": "值",
"label.view": "查看",
"label.view-details": "查看更多",
"label.view-only": "仅浏览量",
"label.views": "浏览量",
"label.views-per-visit": "Views per visit",
"label.views-per-visit": "每次访问的浏览量",
"label.visitors": "访客",
"label.visits": "Visits",
"label.visits": "访问次数",
"label.website": "网站",
"label.website-id": "网站 ID",
"label.websites": "网站",
-82
View File
@@ -1,82 +0,0 @@
import { User, Website } from '@prisma/client';
import redis from '@umami/redis-client';
import { getSession, getUser, getWebsite } from '../queries';
async function fetchWebsite(websiteId: string): Promise<Website> {
return redis.client.getCache(`website:${websiteId}`, () => getWebsite(websiteId), 86400);
}
async function storeWebsite(data: { id: any }) {
const { id } = data;
const key = `website:${id}`;
const obj = await redis.client.setCache(key, data);
await redis.client.expire(key, 86400);
return obj;
}
async function deleteWebsite(id) {
return redis.client.deleteCache(`website:${id}`);
}
async function fetchUser(id): Promise<User> {
return redis.client.getCache(`user:${id}`, () => getUser(id, { includePassword: true }), 86400);
}
async function storeUser(data) {
const { id } = data;
const key = `user:${id}`;
const obj = await redis.client.setCache(key, data);
await redis.client.expire(key, 86400);
return obj;
}
async function deleteUser(id) {
return redis.client.deleteCache(`user:${id}`);
}
async function fetchSession(id) {
return redis.client.getCache(`session:${id}`, () => getSession(id), 86400);
}
async function storeSession(data) {
const { id } = data;
const key = `session:${id}`;
const obj = await redis.client.setCache(key, data);
await redis.client.expire(key, 86400);
return obj;
}
async function deleteSession(id) {
return redis.client.deleteCache(`session:${id}`);
}
async function fetchUserBlock(userId: string) {
const key = `user:block:${userId}`;
return redis.client.get(key);
}
async function incrementUserBlock(userId: string) {
const key = `user:block:${userId}`;
return redis.client.incr(key);
}
export default {
fetchWebsite,
storeWebsite,
deleteWebsite,
fetchUser,
storeUser,
deleteUser,
fetchSession,
storeSession,
deleteSession,
fetchUserBlock,
incrementUserBlock,
enabled: !!redis.enabled,
};
+2 -2
View File
@@ -4,7 +4,7 @@ import debug from 'debug';
import { CLICKHOUSE } from 'lib/db';
import { QueryFilters, QueryOptions } from './types';
import { OPERATORS } from './constants';
import { loadWebsite } from './load';
import { fetchWebsite } from './load';
import { maxDate } from './date';
import { filtersToArray } from './params';
@@ -106,7 +106,7 @@ function getFilterParams(filters: QueryFilters = {}) {
}
async function parseFilters(websiteId: string, filters: QueryFilters = {}, options?: QueryOptions) {
const website = await loadWebsite(websiteId);
const website = await fetchWebsite(websiteId);
return {
filterQuery: getFilterQuery(filters, options),
+1 -1
View File
@@ -30,7 +30,7 @@ export const FILTER_DAY = 'filter-day';
export const FILTER_RANGE = 'filter-range';
export const FILTER_REFERRERS = 'filter-referrers';
export const FILTER_PAGES = 'filter-pages';
export const UNIT_TYPES = ['year', 'month', 'hour', 'day'];
export const UNIT_TYPES = ['year', 'month', 'hour', 'day', 'minute'];
export const EVENT_COLUMNS = ['url', 'referrer', 'title', 'query', 'event'];
export const SESSION_COLUMNS = [
+1 -5
View File
@@ -1,6 +1,6 @@
import { startOfHour, startOfMonth } from 'date-fns';
import { hash } from 'next-basics';
import { v4, v5, validate } from 'uuid';
import { v4, v5 } from 'uuid';
export function secret() {
return hash(process.env.APP_SECRET || process.env.DATABASE_URL);
@@ -23,7 +23,3 @@ export function uuid(...args: any) {
return v5(hash(...args, salt()), v5.DNS);
}
export function isUuid(value: string) {
return validate(value);
}
+1 -1
View File
@@ -132,7 +132,7 @@ export async function getClientInfo(req: NextApiRequestCollect) {
const subdivision2 = location?.subdivision2;
const city = location?.city;
const browser = browserName(userAgent);
const os = detectOS(userAgent);
const os = detectOS(userAgent) as string;
const device = getDevice(req.body?.payload?.screen, os);
return { userAgent, browser, os, ip, country, subdivision1, subdivision2, city, device };
+2
View File
@@ -2,6 +2,7 @@ import {
arSA,
be,
bn,
bs,
cs,
sk,
da,
@@ -48,6 +49,7 @@ export const languages = {
'ar-SA': { label: 'العربية', dateLocale: arSA, dir: 'rtl' },
'be-BY': { label: 'Беларуская', dateLocale: be },
'bn-BD': { label: 'বাংলা', dateLocale: bn },
'bs-BA': { label: 'Bosanski', dateLocale: bs },
'ca-ES': { label: 'Català', dateLocale: ca },
'cs-CZ': { label: 'Čeština', dateLocale: cs },
'da-DK': { label: 'Dansk', dateLocale: da },
+9 -25
View File
@@ -1,12 +1,12 @@
import cache from 'lib/cache';
import { getSession, getUser, getWebsite } from 'queries';
import { User, Website, Session } from '@prisma/client';
import { getSession, getWebsite } from 'queries';
import { Website, Session } from '@prisma/client';
import redis from '@umami/redis-client';
export async function loadWebsite(websiteId: string): Promise<Website> {
export async function fetchWebsite(websiteId: string): Promise<Website> {
let website;
if (cache.enabled) {
website = await cache.fetchWebsite(websiteId);
if (redis.enabled) {
website = await redis.client.fetch(`website:${websiteId}`, () => getWebsite(websiteId), 86400);
} else {
website = await getWebsite(websiteId);
}
@@ -18,11 +18,11 @@ export async function loadWebsite(websiteId: string): Promise<Website> {
return website;
}
export async function loadSession(sessionId: string): Promise<Session> {
export async function fetchSession(sessionId: string): Promise<Session> {
let session;
if (cache.enabled) {
session = await cache.fetchSession(sessionId);
if (redis.enabled) {
session = await redis.client.fetch(`session:${sessionId}`, () => getSession(sessionId), 86400);
} else {
session = await getSession(sessionId);
}
@@ -33,19 +33,3 @@ export async function loadSession(sessionId: string): Promise<Session> {
return session;
}
export async function loadUser(userId: string): Promise<User> {
let user;
if (cache.enabled) {
user = await cache.fetchUser(userId);
} else {
user = await getUser(userId);
}
if (!user || user.deletedAt) {
return null;
}
return user;
}
+5 -9
View File
@@ -4,13 +4,12 @@ import redis from '@umami/redis-client';
import { getAuthToken, parseShareToken } from 'lib/auth';
import { ROLES } from 'lib/constants';
import { secret } from 'lib/crypto';
import { findSession } from 'lib/session';
import { getSession } from 'lib/session';
import {
badRequest,
createMiddleware,
forbidden,
notFound,
parseSecureToken,
tooManyRequest,
unauthorized,
} from 'next-basics';
import { NextApiRequestCollect } from 'pages/api/send';
@@ -27,7 +26,7 @@ export const useCors = createMiddleware(
export const useSession = createMiddleware(async (req, res, next) => {
try {
const session = await findSession(req as NextApiRequestCollect);
const session = await getSession(req as NextApiRequestCollect);
if (!session) {
log('useSession: Session not found');
@@ -36,11 +35,8 @@ export const useSession = createMiddleware(async (req, res, next) => {
(req as any).session = session;
} catch (e: any) {
if (e.message === 'Usage Limit.') {
return tooManyRequest(res, e.message);
}
if (e.message.startsWith('Website not found:')) {
return forbidden(res, e.message);
if (e.message.startsWith('Website not found')) {
return notFound(res, e.message);
}
return badRequest(res, e.message);
}
+4 -4
View File
@@ -3,9 +3,9 @@ import prisma from '@umami/prisma-client';
import moment from 'moment-timezone';
import { MYSQL, POSTGRESQL, getDatabaseType } from 'lib/db';
import { SESSION_COLUMNS, OPERATORS, DEFAULT_PAGE_SIZE } from './constants';
import { loadWebsite } from './load';
import { fetchWebsite } from './load';
import { maxDate } from './date';
import { QueryFilters, QueryOptions, SearchFilter } from './types';
import { QueryFilters, QueryOptions, PageParams } from './types';
import { filtersToArray } from './params';
const MYSQL_DATE_FORMATS = {
@@ -152,7 +152,7 @@ async function parseFilters(
filters: QueryFilters = {},
options: QueryOptions = {},
) {
const website = await loadWebsite(websiteId);
const website = await fetchWebsite(websiteId);
const joinSession = Object.keys(filters).find(key => SESSION_COLUMNS.includes(key));
return {
@@ -191,7 +191,7 @@ async function rawQuery(sql: string, data: object): Promise<any> {
return prisma.rawQuery(query, params);
}
async function pagedQuery<T>(model: string, criteria: T, filters: SearchFilter) {
async function pagedQuery<T>(model: string, criteria: T, filters: PageParams) {
const { page = 1, pageSize, orderBy, sortDescending = false } = filters || {};
const size = +pageSize || DEFAULT_PAGE_SIZE;
+1 -1
View File
@@ -2,7 +2,7 @@ import * as yup from 'yup';
export const dateRange = {
startAt: yup.number().integer().required(),
endAt: yup.number().integer().moreThan(yup.ref('startAt')).required(),
endAt: yup.number().integer().min(yup.ref('startAt')).required(),
};
export const pageInfo = {
+9 -46
View File
@@ -1,28 +1,13 @@
import { isUuid, secret, uuid, visitSalt } from 'lib/crypto';
import { secret, uuid, visitSalt } from 'lib/crypto';
import { getClientInfo } from 'lib/detect';
import { parseToken } from 'next-basics';
import { NextApiRequestCollect } from 'pages/api/send';
import { createSession } from 'queries';
import cache from './cache';
import clickhouse from './clickhouse';
import { loadSession, loadWebsite } from './load';
import { fetchSession, fetchWebsite } from './load';
import { SessionData } from 'lib/types';
export async function findSession(req: NextApiRequestCollect): Promise<{
id: any;
websiteId: string;
visitId: string;
hostname: string;
browser: string;
os: any;
device: string;
screen: string;
language: string;
country: any;
subdivision1: any;
subdivision2: any;
city: any;
ownerId: string;
}> {
export async function getSession(req: NextApiRequestCollect): Promise<SessionData> {
const { payload } = req.body;
if (!payload) {
@@ -35,9 +20,8 @@ export async function findSession(req: NextApiRequestCollect): Promise<{
if (cacheToken) {
const result = await parseToken(cacheToken, secret());
// Token is valid
if (result) {
await checkUserBlock(result?.ownerId);
return result;
}
}
@@ -45,25 +29,13 @@ export async function findSession(req: NextApiRequestCollect): Promise<{
// Verify payload
const { website: websiteId, hostname, screen, language } = payload;
// Check the hostname value for legality to eliminate dirty data
const validHostnameRegex = /^[\w-.]+$/;
if (!validHostnameRegex.test(hostname)) {
throw new Error('Invalid hostname.');
}
if (!isUuid(websiteId)) {
throw new Error('Invalid website ID.');
}
// Find website
const website = await loadWebsite(websiteId);
const website = await fetchWebsite(websiteId);
if (!website) {
throw new Error(`Website not found: ${websiteId}.`);
}
await checkUserBlock(website.userId);
const { userAgent, browser, os, ip, country, subdivision1, subdivision2, city, device } =
await getClientInfo(req);
@@ -78,7 +50,7 @@ export async function findSession(req: NextApiRequestCollect): Promise<{
visitId,
hostname,
browser,
os: os as any,
os,
device,
screen,
language,
@@ -86,12 +58,11 @@ export async function findSession(req: NextApiRequestCollect): Promise<{
subdivision1,
subdivision2,
city,
ownerId: website.userId,
};
}
// Find session
let session = await loadSession(sessionId);
let session = await fetchSession(sessionId);
// Create a session if not found
if (!session) {
@@ -117,13 +88,5 @@ export async function findSession(req: NextApiRequestCollect): Promise<{
}
}
return { ...session, ownerId: website.userId, visitId: visitId };
}
async function checkUserBlock(userId: string) {
if (process.env.ENABLE_BLOCKER && (await cache.fetchUserBlock(userId))) {
await cache.incrementUserBlock(userId);
throw new Error('Usage Limit.');
}
return { ...session, visitId: visitId };
}
+21 -29
View File
@@ -24,31 +24,7 @@ export type DynamicDataType = ObjectValues<typeof DATA_TYPE>;
export type KafkaTopic = ObjectValues<typeof KAFKA_TOPIC>;
export type ReportType = ObjectValues<typeof REPORT_TYPES>;
export interface WebsiteSearchFilter extends SearchFilter {
userId?: string;
teamId?: string;
includeTeams?: boolean;
onlyTeams?: boolean;
}
export interface UserSearchFilter extends SearchFilter {
teamId?: string;
}
export interface TeamSearchFilter extends SearchFilter {
userId?: string;
}
export interface TeamUserSearchFilter extends SearchFilter {
teamId?: string;
}
export interface ReportSearchFilter extends SearchFilter {
userId?: string;
websiteId?: string;
}
export interface SearchFilter {
export interface PageParams {
query?: string;
page?: number;
pageSize?: number;
@@ -56,7 +32,7 @@ export interface SearchFilter {
sortDescending?: boolean;
}
export interface FilterResult<T> {
export interface PageResult<T> {
data: T;
count: number;
page: number;
@@ -66,10 +42,10 @@ export interface FilterResult<T> {
}
export interface FilterQueryResult<T> {
result: FilterResult<T>;
result: PageResult<T>;
query: any;
params: SearchFilter;
setParams: Dispatch<SetStateAction<T | SearchFilter>>;
params: PageParams;
setParams: Dispatch<SetStateAction<T | PageParams>>;
}
export interface DynamicData {
@@ -230,3 +206,19 @@ export interface RealtimeData {
countries?: any[];
visitors?: any[];
}
export interface SessionData {
id: string;
websiteId: string;
visitId: string;
hostname: string;
browser: string;
os: string;
device: string;
screen: string;
language: string;
country: string;
subdivision1: string;
subdivision2: string;
city: string;
}
+2 -2
View File
@@ -1,13 +1,13 @@
import { canViewUsers } from 'lib/auth';
import { useAuth, useValidate } from 'lib/middleware';
import { NextApiRequestQueryBody, Role, SearchFilter, User } from 'lib/types';
import { NextApiRequestQueryBody, Role, PageParams, User } from 'lib/types';
import { pageInfo } from 'lib/schema';
import { NextApiResponse } from 'next';
import { methodNotAllowed, ok, unauthorized } from 'next-basics';
import { getUsers } from 'queries';
import * as yup from 'yup';
export interface UsersRequestQuery extends SearchFilter {}
export interface UsersRequestQuery extends PageParams {}
export interface UsersRequestBody {
userId: string;
username: string;
+40 -3
View File
@@ -1,13 +1,17 @@
import { canViewAllWebsites } from 'lib/auth';
import { ROLES } from 'lib/constants';
import { useAuth, useCors, useValidate } from 'lib/middleware';
import { NextApiRequestQueryBody, SearchFilter } from 'lib/types';
import { pageInfo } from 'lib/schema';
import { NextApiRequestQueryBody, PageParams } from 'lib/types';
import { NextApiResponse } from 'next';
import { methodNotAllowed, ok, unauthorized } from 'next-basics';
import { getWebsites } from 'queries';
import * as yup from 'yup';
import { pageInfo } from 'lib/schema';
export interface WebsitesRequestQuery extends SearchFilter {}
export interface WebsitesRequestQuery extends PageParams {
userId?: string;
includeTeams?: boolean;
}
export interface WebsitesRequestBody {
name: string;
@@ -39,8 +43,29 @@ export default async (
return unauthorized(res);
}
const { userId, includeOwnedTeams } = req.query;
const websites = await getWebsites(
{
where: {
OR: [
...(userId && [{ userId }]),
...(userId &&
includeOwnedTeams && [
{
team: {
deletedAt: null,
teamUser: {
some: {
role: ROLES.teamOwner,
userId,
},
},
},
},
]),
],
},
include: {
user: {
select: {
@@ -48,6 +73,18 @@ export default async (
id: true,
},
},
team: {
where: {
deletedAt: null,
},
include: {
teamUser: {
where: {
role: ROLES.teamOwner,
},
},
},
},
},
},
req.query,
+1 -1
View File
@@ -17,7 +17,7 @@ const schema = {
GET: yup.object().shape({
websiteId: yup.string().uuid().required(),
startAt: yup.number().integer().required(),
endAt: yup.number().integer().moreThan(yup.ref('startAt')).required(),
endAt: yup.number().integer().min(yup.ref('startAt')).required(),
event: yup.string(),
}),
};
+1 -1
View File
@@ -17,7 +17,7 @@ const schema = {
GET: yup.object().shape({
websiteId: yup.string().uuid().required(),
startAt: yup.number().integer().required(),
endAt: yup.number().integer().moreThan(yup.ref('startAt')).required(),
endAt: yup.number().integer().min(yup.ref('startAt')).required(),
field: yup.string(),
}),
};
+1 -1
View File
@@ -16,7 +16,7 @@ const schema = {
GET: yup.object().shape({
websiteId: yup.string().uuid().required(),
startAt: yup.number().integer().required(),
endAt: yup.number().integer().moreThan(yup.ref('startAt')).required(),
endAt: yup.number().integer().min(yup.ref('startAt')).required(),
}),
};
+5 -6
View File
@@ -1,10 +1,5 @@
import ipaddr from 'ipaddr.js';
import { isbot } from 'isbot';
import { COLLECTION_TYPE, HOSTNAME_REGEX, IP_REGEX } from 'lib/constants';
import { secret, visitSalt, uuid } from 'lib/crypto';
import { getIpAddress } from 'lib/detect';
import { useCors, useSession, useValidate } from 'lib/middleware';
import { CollectionType, YupRequest } from 'lib/types';
import { NextApiRequest, NextApiResponse } from 'next';
import {
badRequest,
@@ -15,6 +10,11 @@ import {
safeDecodeURI,
send,
} from 'next-basics';
import { COLLECTION_TYPE, HOSTNAME_REGEX, IP_REGEX } from 'lib/constants';
import { secret, visitSalt, uuid } from 'lib/crypto';
import { getIpAddress } from 'lib/detect';
import { useCors, useSession, useValidate } from 'lib/middleware';
import { CollectionType, YupRequest } from 'lib/types';
import { saveEvent, saveSessionData } from 'queries';
import * as yup from 'yup';
@@ -41,7 +41,6 @@ export interface NextApiRequestCollect extends NextApiRequest {
id: string;
websiteId: string;
visitId: string;
ownerId: string;
hostname: string;
browser: string;
os: string;
+2 -2
View File
@@ -1,13 +1,13 @@
import { canAddUserToTeam, canViewTeam } from 'lib/auth';
import { useAuth, useValidate } from 'lib/middleware';
import { pageInfo } from 'lib/schema';
import { NextApiRequestQueryBody, SearchFilter } from 'lib/types';
import { NextApiRequestQueryBody, PageParams } from 'lib/types';
import { NextApiResponse } from 'next';
import { badRequest, methodNotAllowed, ok, unauthorized } from 'next-basics';
import { createTeamUser, getTeamUser, getTeamUsers } from 'queries';
import * as yup from 'yup';
export interface TeamUserRequestQuery extends SearchFilter {
export interface TeamUserRequestQuery extends PageParams {
teamId: string;
}
@@ -1,13 +1,13 @@
import * as yup from 'yup';
import { canViewTeam } from 'lib/auth';
import { useAuth, useValidate } from 'lib/middleware';
import { NextApiRequestQueryBody, SearchFilter } from 'lib/types';
import { NextApiRequestQueryBody, PageParams } from 'lib/types';
import { pageInfo } from 'lib/schema';
import { NextApiResponse } from 'next';
import { ok, unauthorized } from 'next-basics';
import { getTeamWebsites } from 'queries';
export interface TeamWebsiteRequestQuery extends SearchFilter {
export interface TeamWebsiteRequestQuery extends PageParams {
teamId: string;
}
+2 -2
View File
@@ -3,13 +3,13 @@ import { Team } from '@prisma/client';
import { canCreateTeam } from 'lib/auth';
import { uuid } from 'lib/crypto';
import { useAuth, useValidate } from 'lib/middleware';
import { NextApiRequestQueryBody, SearchFilter } from 'lib/types';
import { NextApiRequestQueryBody, PageParams } from 'lib/types';
import { pageInfo } from 'lib/schema';
import { NextApiResponse } from 'next';
import { getRandomChars, methodNotAllowed, ok, unauthorized } from 'next-basics';
import { createTeam } from 'queries';
export interface TeamsRequestQuery extends SearchFilter {}
export interface TeamsRequestQuery extends PageParams {}
export interface TeamsRequestBody {
name: string;
}
+2 -2
View File
@@ -1,12 +1,12 @@
import * as yup from 'yup';
import { useAuth, useCors, useValidate } from 'lib/middleware';
import { NextApiRequestQueryBody, SearchFilter } from 'lib/types';
import { NextApiRequestQueryBody, PageParams } from 'lib/types';
import { pageInfo } from 'lib/schema';
import { NextApiResponse } from 'next';
import { methodNotAllowed, ok, unauthorized } from 'next-basics';
import { getUserTeams } from 'queries';
export interface UserTeamsRequestQuery extends SearchFilter {
export interface UserTeamsRequestQuery extends PageParams {
userId: string;
}
+1 -1
View File
@@ -26,7 +26,7 @@ const schema = {
GET: yup.object().shape({
id: yup.string().uuid().required(),
startAt: yup.number().integer().required(),
endAt: yup.number().integer().moreThan(yup.ref<number>('startAt')).required(),
endAt: yup.number().integer().min(yup.ref<number>('startAt')).required(),
}),
};
+2 -2
View File
@@ -2,14 +2,14 @@ import { canCreateUser } from 'lib/auth';
import { ROLES } from 'lib/constants';
import { uuid } from 'lib/crypto';
import { useAuth, useValidate } from 'lib/middleware';
import { NextApiRequestQueryBody, Role, SearchFilter, User } from 'lib/types';
import { NextApiRequestQueryBody, Role, PageParams, User } from 'lib/types';
import { pageInfo } from 'lib/schema';
import { NextApiResponse } from 'next';
import { badRequest, hashPassword, methodNotAllowed, ok, unauthorized } from 'next-basics';
import { createUser, getUserByUsername } from 'queries';
import * as yup from 'yup';
export interface UsersRequestQuery extends SearchFilter {}
export interface UsersRequestQuery extends PageParams {}
export interface UsersRequestBody {
username: string;
password: string;
+24 -6
View File
@@ -1,6 +1,6 @@
import { canViewWebsite } from 'lib/auth';
import { useAuth, useCors, useValidate } from 'lib/middleware';
import { getRequestDateRange } from 'lib/request';
import { getRequestFilters, getRequestDateRange } from 'lib/request';
import { NextApiRequestQueryBody, WebsiteMetric } from 'lib/types';
import { TimezoneTest, UnitTypeTest } from 'lib/yup';
import { NextApiResponse } from 'next';
@@ -15,16 +15,32 @@ export interface WebsiteEventsRequestQuery {
unit?: string;
timezone?: string;
url: string;
referrer?: string;
title?: string;
os?: string;
browser?: string;
device?: string;
country?: string;
region: string;
city?: string;
}
const schema = {
GET: yup.object().shape({
websiteId: yup.string().uuid().required(),
startAt: yup.number().integer().required(),
endAt: yup.number().integer().moreThan(yup.ref('startAt')).required(),
endAt: yup.number().integer().min(yup.ref('startAt')).required(),
unit: UnitTypeTest,
timezone: TimezoneTest,
url: yup.string(),
referrer: yup.string(),
title: yup.string(),
os: yup.string(),
browser: yup.string(),
device: yup.string(),
country: yup.string(),
region: yup.string(),
city: yup.string(),
}),
};
@@ -36,7 +52,7 @@ export default async (
await useAuth(req, res);
await useValidate(schema, req, res);
const { websiteId, timezone, url } = req.query;
const { websiteId, timezone } = req.query;
const { startDate, endDate, unit } = await getRequestDateRange(req);
if (req.method === 'GET') {
@@ -44,13 +60,15 @@ export default async (
return unauthorized(res);
}
const events = await getEventMetrics(websiteId, {
const filters = {
...getRequestFilters(req),
startDate,
endDate,
timezone,
unit,
url,
});
};
const events = await getEventMetrics(websiteId, filters);
return ok(res, events);
}
@@ -1,13 +1,13 @@
import * as yup from 'yup';
import { canViewWebsite } from 'lib/auth';
import { useAuth, useCors, useValidate } from 'lib/middleware';
import { NextApiRequestQueryBody, SearchFilter } from 'lib/types';
import { NextApiRequestQueryBody, PageParams } from 'lib/types';
import { NextApiResponse } from 'next';
import { methodNotAllowed, ok, unauthorized } from 'next-basics';
import { getWebsiteReports } from 'queries';
import { pageInfo } from 'lib/schema';
export interface ReportsRequestQuery extends SearchFilter {
export interface ReportsRequestQuery extends PageParams {
websiteId: string;
}
+2 -2
View File
@@ -1,7 +1,7 @@
import { canCreateTeamWebsite, canCreateWebsite } from 'lib/auth';
import { uuid } from 'lib/crypto';
import { useAuth, useCors, useValidate } from 'lib/middleware';
import { NextApiRequestQueryBody, SearchFilter } from 'lib/types';
import { NextApiRequestQueryBody, PageParams } from 'lib/types';
import { NextApiResponse } from 'next';
import { methodNotAllowed, ok, unauthorized } from 'next-basics';
import { createWebsite } from 'queries';
@@ -9,7 +9,7 @@ import userWebsitesRoute from 'pages/api/users/[userId]/websites';
import * as yup from 'yup';
import { pageInfo } from 'lib/schema';
export interface WebsitesRequestQuery extends SearchFilter {}
export interface WebsitesRequestQuery extends PageParams {}
export interface WebsitesRequestBody {
name: string;
+7 -7
View File
@@ -1,6 +1,6 @@
import { Prisma, Report } from '@prisma/client';
import prisma from 'lib/prisma';
import { FilterResult, ReportSearchFilter } from 'lib/types';
import { PageResult, PageParams } from 'lib/types';
import ReportFindManyArgs = Prisma.ReportFindManyArgs;
async function findReport(criteria: Prisma.ReportFindUniqueArgs): Promise<Report> {
@@ -17,8 +17,8 @@ export async function getReport(reportId: string): Promise<Report> {
export async function getReports(
criteria: ReportFindManyArgs,
filters: ReportSearchFilter = {},
): Promise<FilterResult<Report[]>> {
filters: PageParams = {},
): Promise<PageResult<Report[]>> {
const { query } = filters;
const where: Prisma.ReportWhereInput = {
@@ -50,8 +50,8 @@ export async function getReports(
export async function getUserReports(
userId: string,
filters?: ReportSearchFilter,
): Promise<FilterResult<Report[]>> {
filters?: PageParams,
): Promise<PageResult<Report[]>> {
return getReports(
{
where: {
@@ -72,8 +72,8 @@ export async function getUserReports(
export async function getWebsiteReports(
websiteId: string,
filters: ReportSearchFilter = {},
): Promise<FilterResult<Report[]>> {
filters: PageParams = {},
): Promise<PageResult<Report[]>> {
return getReports(
{
where: {
+4 -4
View File
@@ -2,7 +2,7 @@ import { Prisma, Team } from '@prisma/client';
import { ROLES } from 'lib/constants';
import { uuid } from 'lib/crypto';
import prisma from 'lib/prisma';
import { FilterResult, TeamSearchFilter } from 'lib/types';
import { PageResult, PageParams } from 'lib/types';
import TeamFindManyArgs = Prisma.TeamFindManyArgs;
export async function findTeam(criteria: Prisma.TeamFindUniqueArgs): Promise<Team> {
@@ -22,8 +22,8 @@ export async function getTeam(teamId: string, options: { includeMembers?: boolea
export async function getTeams(
criteria: TeamFindManyArgs,
filters: TeamSearchFilter = {},
): Promise<FilterResult<Team[]>> {
filters: PageParams = {},
): Promise<PageResult<Team[]>> {
const { getSearchParameters } = prisma;
const { query } = filters;
@@ -42,7 +42,7 @@ export async function getTeams(
);
}
export async function getUserTeams(userId: string, filters: TeamSearchFilter = {}) {
export async function getUserTeams(userId: string, filters: PageParams = {}) {
return getTeams(
{
where: {
+3 -3
View File
@@ -1,7 +1,7 @@
import { Prisma, TeamUser } from '@prisma/client';
import { uuid } from 'lib/crypto';
import prisma from 'lib/prisma';
import { FilterResult, TeamUserSearchFilter } from 'lib/types';
import { PageResult, PageParams } from 'lib/types';
import TeamUserFindManyArgs = Prisma.TeamUserFindManyArgs;
export async function findTeamUser(criteria: Prisma.TeamUserFindUniqueArgs): Promise<TeamUser> {
@@ -19,8 +19,8 @@ export async function getTeamUser(teamId: string, userId: string): Promise<TeamU
export async function getTeamUsers(
criteria: TeamUserFindManyArgs,
filters?: TeamUserSearchFilter,
): Promise<FilterResult<TeamUser[]>> {
filters?: PageParams,
): Promise<PageResult<TeamUser[]>> {
const { query } = filters;
const where: Prisma.TeamUserWhereInput = {
+4 -15
View File
@@ -1,8 +1,7 @@
import { Prisma } from '@prisma/client';
import cache from 'lib/cache';
import { ROLES } from 'lib/constants';
import prisma from 'lib/prisma';
import { FilterResult, Role, User, UserSearchFilter } from 'lib/types';
import { PageResult, Role, User, PageParams } from 'lib/types';
import { getRandomChars } from 'next-basics';
import UserFindManyArgs = Prisma.UserFindManyArgs;
@@ -50,8 +49,8 @@ export async function getUserByUsername(username: string, options: GetUserOption
export async function getUsers(
criteria: UserFindManyArgs,
filters?: UserSearchFilter,
): Promise<FilterResult<User[]>> {
filters?: PageParams,
): Promise<PageResult<User[]>> {
const { query } = filters;
const where: Prisma.UserWhereInput = {
@@ -221,15 +220,5 @@ export async function deleteUser(
id: userId,
},
}),
]).then(async data => {
if (cache.enabled) {
const ids = websites.map(a => a.id);
for (let i = 0; i < ids.length; i++) {
await cache.deleteWebsite(`website:${ids[i]}`);
}
}
return data;
});
]);
}
+12 -33
View File
@@ -1,7 +1,6 @@
import { Prisma, Website } from '@prisma/client';
import cache from 'lib/cache';
import prisma from 'lib/prisma';
import { FilterResult, WebsiteSearchFilter } from 'lib/types';
import { PageResult, PageParams } from 'lib/types';
import WebsiteFindManyArgs = Prisma.WebsiteFindManyArgs;
async function findWebsite(criteria: Prisma.WebsiteFindUniqueArgs): Promise<Website> {
@@ -26,8 +25,8 @@ export async function getSharedWebsite(shareId: string) {
export async function getWebsites(
criteria: WebsiteFindManyArgs,
filters: WebsiteSearchFilter,
): Promise<FilterResult<Website[]>> {
filters: PageParams,
): Promise<PageResult<Website[]>> {
const { query } = filters;
const where: Prisma.WebsiteWhereInput = {
@@ -54,8 +53,8 @@ export async function getAllWebsites(userId: string) {
export async function getUserWebsites(
userId: string,
filters?: WebsiteSearchFilter,
): Promise<FilterResult<Website[]>> {
filters?: PageParams,
): Promise<PageResult<Website[]>> {
return getWebsites(
{
where: {
@@ -79,8 +78,8 @@ export async function getUserWebsites(
export async function getTeamWebsites(
teamId: string,
filters?: WebsiteSearchFilter,
): Promise<FilterResult<Website[]>> {
filters?: PageParams,
): Promise<PageResult<Website[]>> {
return getWebsites(
{
where: {
@@ -102,17 +101,9 @@ export async function getTeamWebsites(
export async function createWebsite(
data: Prisma.WebsiteCreateInput | Prisma.WebsiteUncheckedCreateInput,
): Promise<Website> {
return prisma.client.website
.create({
data,
})
.then(async data => {
if (cache.enabled) {
await cache.storeWebsite(data);
}
return data;
});
return prisma.client.website.create({
data,
});
}
export async function updateWebsite(
@@ -148,13 +139,7 @@ export async function resetWebsite(
resetAt: new Date(),
},
}),
]).then(async data => {
if (cache.enabled) {
await cache.storeWebsite(data[3]);
}
return data;
});
]);
}
export async function deleteWebsite(
@@ -188,11 +173,5 @@ export async function deleteWebsite(
: client.website.delete({
where: { id: websiteId },
}),
]).then(async data => {
if (cache.enabled) {
await cache.deleteWebsite(websiteId);
}
return data;
});
]);
}
@@ -33,7 +33,7 @@ async function relationalQuery(websiteId: string, filters: QueryFilters) {
and event_data.created_at between {{startDate}} and {{endDate}}
and website_event.event_name = {{event}}
group by website_event.event_name, event_data.data_key, event_data.data_type, event_data.string_value
order by 1 asc, 2 asc, 3 asc, 4 desc
order by 1 asc, 2 asc, 3 asc, 5 desc
`,
params,
);
@@ -81,7 +81,7 @@ async function clickhouseQuery(
and created_at between {startDate:DateTime64} and {endDate:DateTime64}
and event_name = {event:String}
group by data_key, data_type, string_value, event_name
order by 1 asc, 2 asc, 3 asc, 4 desc
order by 1 asc, 2 asc, 3 asc, 5 desc
limit 500
`,
params,
@@ -25,12 +25,12 @@ async function relationalQuery(websiteId: string, filters: QueryFilters) {
`
select
event_name x,
${getDateQuery('created_at', unit, timezone)} t,
${getDateQuery('website_event.created_at', unit, timezone)} t,
count(*) y
from website_event
${joinSession}
where website_id = {{websiteId::uuid}}
and created_at between {{startDate}} and {{endDate}}
where website_event.website_id = {{websiteId::uuid}}
and website_event.created_at between {{startDate}} and {{endDate}}
and event_type = {{eventType}}
${filterQuery}
group by 1, 2
+16 -25
View File
@@ -1,5 +1,4 @@
import { Prisma } from '@prisma/client';
import cache from 'lib/cache';
import prisma from 'lib/prisma';
export async function createSession(data: Prisma.SessionCreateInput) {
@@ -18,28 +17,20 @@ export async function createSession(data: Prisma.SessionCreateInput) {
city,
} = data;
return prisma.client.session
.create({
data: {
id,
websiteId,
hostname,
browser,
os,
device,
screen,
language,
country,
subdivision1,
subdivision2,
city,
},
})
.then(async data => {
if (cache.enabled) {
await cache.storeSession(data);
}
return data;
});
return prisma.client.session.create({
data: {
id,
websiteId,
hostname,
browser,
os,
device,
screen,
language,
country,
subdivision1,
subdivision2,
city,
},
});
}
+31 -1091
View File
File diff suppressed because it is too large Load Diff