From 8cc571f548d2a28b50d86bd7540a555c1f8a5e92 Mon Sep 17 00:00:00 2001 From: GochoMugo Date: Thu, 11 Dec 2025 08:53:13 +0300 Subject: [PATCH 01/13] fix: handle denied storage access --- src/tracker/index.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/tracker/index.js b/src/tracker/index.js index ad3648ac..85d27430 100644 --- a/src/tracker/index.js +++ b/src/tracker/index.js @@ -12,7 +12,13 @@ if (!currentScript) return; const { hostname, href, origin } = location; - const localStorage = href.startsWith('data:') ? undefined : window.localStorage; + + let localStorage; + try { + localStorage = href.startsWith('data:') ? undefined : window.localStorage; + } catch { + /* (DOMException) SecurityError: Access is denied for this document. */ + } const _data = 'data-'; const _false = 'false'; From b088a2ee6edb05ee5ccf103c2b2a2fe2a7721049 Mon Sep 17 00:00:00 2001 From: Kristofor Carle Date: Thu, 11 Dec 2025 14:53:08 -0500 Subject: [PATCH 02/13] fix prisma session race condition error --- src/queries/sql/sessions/saveSessionData.ts | 36 ++++++++------------- 1 file changed, 14 insertions(+), 22 deletions(-) diff --git a/src/queries/sql/sessions/saveSessionData.ts b/src/queries/sql/sessions/saveSessionData.ts index 74093177..cce6cd28 100644 --- a/src/queries/sql/sessions/saveSessionData.ts +++ b/src/queries/sql/sessions/saveSessionData.ts @@ -46,31 +46,23 @@ export async function relationalQuery({ createdAt, })); - const existing = await client.sessionData.findMany({ - where: { - sessionId, - }, - select: { - id: true, - sessionId: true, - dataKey: true, - }, - }); - for (const data of flattenedData) { const { sessionId, dataKey, ...props } = data; - const record = existing.find(e => e.sessionId === sessionId && e.dataKey === dataKey); - if (record) { - await client.sessionData.update({ - where: { - id: record.id, - }, - data: { - ...props, - }, - }); - } else { + // Try to update existing record using compound where clause + // This is safer than using id from a previous query due to race conditions + const updateResult = await client.sessionData.updateMany({ + where: { + sessionId, + dataKey, + }, + data: { + ...props, + }, + }); + + // If no record was updated, create a new one + if (updateResult.count === 0) { await client.sessionData.create({ data, }); From 5b97fb908aa867e0320a506923b334f47bdb6608 Mon Sep 17 00:00:00 2001 From: Egor Fedorov Date: Thu, 11 Dec 2025 23:24:01 +0300 Subject: [PATCH 03/13] fix(events): use correct key for event values --- src/components/hooks/queries/useEventDataValuesQuery.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/hooks/queries/useEventDataValuesQuery.ts b/src/components/hooks/queries/useEventDataValuesQuery.ts index 6529e142..db2a2d7e 100644 --- a/src/components/hooks/queries/useEventDataValuesQuery.ts +++ b/src/components/hooks/queries/useEventDataValuesQuery.ts @@ -16,7 +16,7 @@ export function useEventDataValuesQuery( return useQuery({ queryKey: [ 'websites:event-data:values', - { websiteId, event, propertyName, startAt, endAt, unit, timezone, ...filters }, + { websiteId, startAt, endAt, unit, timezone, ...filters, event, propertyName }, ], queryFn: () => get(`/websites/${websiteId}/event-data/values`, { From 5fbef149d04991e4686c95e2ffd9491be54e4d8e Mon Sep 17 00:00:00 2001 From: RaenonX Date: Wed, 10 Dec 2025 13:16:54 +0800 Subject: [PATCH 04/13] Added custom slug for links --- src/app/(main)/links/LinkEditForm.tsx | 65 +++++++++++++-------------- 1 file changed, 30 insertions(+), 35 deletions(-) diff --git a/src/app/(main)/links/LinkEditForm.tsx b/src/app/(main)/links/LinkEditForm.tsx index 6c10c7f0..e9ad18f4 100644 --- a/src/app/(main)/links/LinkEditForm.tsx +++ b/src/app/(main)/links/LinkEditForm.tsx @@ -4,13 +4,14 @@ import { Form, FormField, FormSubmitButton, + Grid, Icon, Label, Loading, Row, TextField, } from '@umami/react-zen'; -import { useEffect, useState } from 'react'; +import { useState } from 'react'; import { useConfig, useLinkQuery, useMessages } from '@/components/hooks'; import { useUpdateQuery } from '@/components/hooks/queries/useUpdateQuery'; import { RefreshCw } from '@/components/icons'; @@ -42,7 +43,7 @@ export function LinkEditForm({ const { linksUrl } = useConfig(); const hostUrl = linksUrl || LINKS_URL; const { data, isLoading } = useLinkQuery(linkId); - const [slug, setSlug] = useState(generateId()); + const [defaultSlug] = useState(generateId()); const handleSubmit = async (data: any) => { await mutateAsync(data, { @@ -55,14 +56,6 @@ export function LinkEditForm({ }); }; - const handleSlug = () => { - const slug = generateId(); - - setSlug(slug); - - return slug; - }; - const checkUrl = (url: string) => { if (!isValidUrl(url)) { return formatMessage(labels.invalidUrl); @@ -70,19 +63,19 @@ export function LinkEditForm({ return true; }; - useEffect(() => { - if (data) { - setSlug(data.slug); - } - }, [data]); - if (linkId && isLoading) { return ; } return ( -
- {({ setValue }) => { + + {({ setValue, watch }) => { + const slug = watch('slug'); + return ( <> - - - + + + + + + @@ -121,14 +124,6 @@ export function LinkEditForm({ allowCopy style={{ width: '100%' }} /> - From 53dfc5e76ad2e3fe8fc103b62a96cdda38ad9aef Mon Sep 17 00:00:00 2001 From: RaenonX Date: Wed, 10 Dec 2025 13:18:11 +0800 Subject: [PATCH 05/13] Added `pm2.yml` in `.gitignore` --- .gitignore | 91 +++++++++++++++++++++++++++--------------------------- 1 file changed, 46 insertions(+), 45 deletions(-) diff --git a/.gitignore b/.gitignore index 753389d1..de893d0f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,45 +1,46 @@ -# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. - -# dependencies -node_modules -.pnp -.pnp.js -.pnpm-store -package-lock.json - -# testing -/coverage - -# next.js -/.next -/out - -# production -/build -/public/script.js -/geo -/dist -/generated -/src/generated - -# misc -.DS_Store -.idea -.yarn -*.iml -*.log -.vscode -.tool-versions - -# debug -npm-debug.log* -yarn-debug.log* -yarn-error.log* - -# local env files -.env -.env.* -*.env.* - -*.dev.yml - +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +node_modules +.pnp +.pnp.js +.pnpm-store +package-lock.json + +# testing +/coverage + +# next.js +/.next +/out + +# production +/build +/public/script.js +/geo +/dist +/generated +/src/generated +pm2.yml + +# misc +.DS_Store +.idea +.yarn +*.iml +*.log +.vscode +.tool-versions + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# local env files +.env +.env.* +*.env.* + +*.dev.yml + From 886544f29752133424acfda2008c2941753f6cf1 Mon Sep 17 00:00:00 2001 From: Jahidul Islam Date: Sat, 13 Dec 2025 16:14:27 +0400 Subject: [PATCH 06/13] Correct UAE emirate names in iso-3166-2.json Updated names of UAE emirates for accuracy. --- public/iso-3166-2.json | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/public/iso-3166-2.json b/public/iso-3166-2.json index 347313d7..2b3b5a80 100644 --- a/public/iso-3166-2.json +++ b/public/iso-3166-2.json @@ -6,13 +6,13 @@ "AD-06": "Sant Julia de Loria", "AD-07": "Andorra la Vella", "AD-08": "Escaldes-Engordany", - "AE-AJ": "'Ajman", - "AE-AZ": "Abu Zaby", - "AE-DU": "Dubayy", - "AE-FU": "Al Fujayrah", - "AE-RK": "Ra's al Khaymah", - "AE-SH": "Ash Shariqah", - "AE-UQ": "Umm al Qaywayn", + "AE-AJ": "Ajman", + "AE-AZ": "Abu Dhabi", + "AE-DU": "Dubai", + "AE-FU": "Al Fujairah", + "AE-RK": "Ras al Khaimah", + "AE-SH": "Sharjah", + "AE-UQ": "Umm al Quwain", "AF-BAL": "Balkh", "AF-BAM": "Bamyan", "AF-BDG": "Badghis", From 437c168e6f28318c12c7823bd21e5823f3672449 Mon Sep 17 00:00:00 2001 From: journry789 Date: Mon, 15 Dec 2025 13:56:39 +0800 Subject: [PATCH 07/13] This resolves the issue of being unable to obtain the client's IP address due to the IPv6 format. --- src/lib/ip.ts | 70 +++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 62 insertions(+), 8 deletions(-) diff --git a/src/lib/ip.ts b/src/lib/ip.ts index 5cd77574..a0e3a825 100644 --- a/src/lib/ip.ts +++ b/src/lib/ip.ts @@ -1,3 +1,5 @@ +import ipaddr from 'ipaddr.js'; + export const IP_ADDRESS_HEADERS = [ 'true-client-ip', // CDN 'cf-connecting-ip', // Cloudflare @@ -13,35 +15,87 @@ export const IP_ADDRESS_HEADERS = [ 'x-forwarded', ]; +/** + * Normalize IP strings to a canonical form: + * - strips IPv4-mapped IPv6 (e.g. ::ffff:192.0.2.1 -> 192.0.2.1) + * - keeps valid IPv4/IPv6 as-is (canonically formatted by ipaddr.js) + */ +function normalizeIp(ip?: string | null) { + if (!ip) return ip; + + try { + const parsed = ipaddr.parse(ip); + + if (parsed.kind() === 'ipv6' && (parsed as ipaddr.IPv6).isIPv4MappedAddress()) { + return (parsed as ipaddr.IPv6).toIPv4Address().toString(); + } + + return parsed.toString(); + } catch { + // Fallback: return original if parsing fails + return ip; + } +} + +function resolveIp(ip?: string | null) { + if (!ip) return ip; + + // First, try as-is + const normalized = normalizeIp(ip); + try { + ipaddr.parse(normalized); + return normalized; + } catch { + // try stripping port (handles IPv4:port; leaves IPv6 intact) + const stripped = stripPort(ip); + if (stripped !== ip) { + const normalizedStripped = normalizeIp(stripped); + try { + ipaddr.parse(normalizedStripped); + return normalizedStripped; + } catch { + return normalizedStripped; + } + } + + return normalized; + } +} + export function getIpAddress(headers: Headers) { const customHeader = process.env.CLIENT_IP_HEADER; if (customHeader && headers.get(customHeader)) { - return headers.get(customHeader); + return resolveIp(headers.get(customHeader)); } - const header = IP_ADDRESS_HEADERS.find(name => { - return headers.get(name); - }); + const header = IP_ADDRESS_HEADERS.find(name => headers.get(name)); + if (!header) { + return undefined; + } const ip = headers.get(header); if (header === 'x-forwarded-for') { - return ip?.split(',')?.[0]?.trim(); + return resolveIp(ip?.split(',')?.[0]?.trim()); } if (header === 'forwarded') { const match = ip.match(/for=(\[?[0-9a-fA-F:.]+\]?)/); if (match) { - return match[1]; + return resolveIp(match[1]); } } - return ip; + return resolveIp(ip); } -export function stripPort(ip: string) { +export function stripPort(ip?: string | null) { + if (!ip) { + return ip; + } + if (ip.startsWith('[')) { const endBracket = ip.indexOf(']'); if (endBracket !== -1) { From ad264f941d056f700e7af40ab1a5d78996379d22 Mon Sep 17 00:00:00 2001 From: Mintihuang <43734212+Mintimate@users.noreply.github.com> Date: Mon, 22 Dec 2025 11:35:28 +0000 Subject: [PATCH 08/13] =?UTF-8?q?feat:=20add=20EdgeOne=20headers=20for=20g?= =?UTF-8?q?eolocation=20detection=E2=80=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/lib/detect.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/lib/detect.ts b/src/lib/detect.ts index 68cb6672..910d122b 100644 --- a/src/lib/detect.ts +++ b/src/lib/detect.ts @@ -28,6 +28,12 @@ const PROVIDER_HEADERS = [ regionHeader: 'cloudfront-viewer-country-region', cityHeader: 'cloudfront-viewer-city', }, + // EdgeOne headers (requires custom request headers in Rule Priorities, see: https://edgeone.ai/document/46151) + { + countryHeader: 'eo-ipcountry', + regionHeader: 'eo-region-code', + cityHeader: 'eo-ipcity', + }, ]; export function getDevice(userAgent: string, screen: string = '') { From 6859f00bf643d1a4f58b46cfe8d32e7d673074f3 Mon Sep 17 00:00:00 2001 From: JiPai Date: Tue, 23 Dec 2025 22:39:33 +0800 Subject: [PATCH 09/13] chore(i18n): update zh-CN translation --- public/intl/messages/zh-CN.json | 210 ++++++++++++++++++++++++++++++-- src/lang/zh-CN.json | 49 ++++++-- 2 files changed, 241 insertions(+), 18 deletions(-) diff --git a/public/intl/messages/zh-CN.json b/public/intl/messages/zh-CN.json index b3d2f3c0..1c3f3240 100644 --- a/public/intl/messages/zh-CN.json +++ b/public/intl/messages/zh-CN.json @@ -5,6 +5,18 @@ "value": "访问代码" } ], + "label.account": [ + { + "type": 0, + "value": "账户" + } + ], + "label.action": [ + { + "type": 0, + "value": "行为" + } + ], "label.actions": [ { "type": 0, @@ -35,12 +47,24 @@ "value": "添加描述" } ], + "label.add-link": [ + { + "type": 0, + "value": "添加链接" + } + ], "label.add-member": [ { "type": 0, "value": "添加成员" } ], + "label.add-pixel": [ + { + "type": 0, + "value": "添加像素" + } + ], "label.add-step": [ { "type": 0, @@ -83,12 +107,24 @@ "value": "所有时间段" } ], + "label.analysis": [ + { + "type": 0, + "value": "分析" + } + ], "label.analytics": [ { "type": 0, "value": "分析" } ], + "label.application": [ + { + "type": 0, + "value": "应用" + } + ], "label.apply": [ { "type": 0, @@ -107,6 +143,12 @@ "value": "查看用户如何与您的营销互动,以及是什么促成了转化。" } ], + "label.audience": [ + { + "type": 0, + "value": "受众" + } + ], "label.average": [ { "type": 0, @@ -125,6 +167,12 @@ "value": "之前" } ], + "label.behavior": [ + { + "type": 0, + "value": "行为" + } + ], "label.boards": [ { "type": 0, @@ -173,12 +221,24 @@ "value": "修改密码" } ], + "label.channel": [ + { + "type": 0, + "value": "渠道" + } + ], "label.channels": [ { "type": 0, "value": "渠道" } ], + "label.chart": [ + { + "type": 0, + "value": "图表" + } + ], "label.cities": [ { "type": 0, @@ -203,6 +263,12 @@ "value": "队列" } ], + "label.cohorts": [ + { + "type": 0, + "value": "队列" + } + ], "label.compare": [ { "type": 0, @@ -317,6 +383,12 @@ "value": "创建者" } ], + "label.criteria": [ + { + "type": 0, + "value": "条件" + } + ], "label.currency": [ { "type": 0, @@ -419,6 +491,12 @@ "value": "台式机" } ], + "label.destination-url": [ + { + "type": 0, + "value": "目标URL" + } + ], "label.details": [ { "type": 0, @@ -455,6 +533,12 @@ "value": "唯一ID" } ], + "label.documentation": [ + { + "type": 0, + "value": "文档" + } + ], "label.does-not-contain": [ { "type": 0, @@ -479,6 +563,12 @@ "value": "域名" } ], + "label.download": [ + { + "type": 0, + "value": "下载" + } + ], "label.dropoff": [ { "type": 0, @@ -506,7 +596,7 @@ "label.email": [ { "type": 0, - "value": "Email" + "value": "邮箱" } ], "label.enable-share-url": [ @@ -527,6 +617,12 @@ "value": "入口 URL" } ], + "label.environment": [ + { + "type": 0, + "value": "环境" + } + ], "label.event": [ { "type": 0, @@ -671,6 +767,12 @@ "value": "分组" } ], + "label.growth": [ + { + "type": 0, + "value": "Growth" + } + ], "label.hostname": [ { "type": 0, @@ -701,6 +803,12 @@ "value": "通过使用筛选器和划分时间段来更深入地研究数据。" } ], + "label.invalid-url": [ + { + "type": 0, + "value": "无效URL" + } + ], "label.is": [ { "type": 0, @@ -863,12 +971,24 @@ "value": "少于等于" } ], + "label.link": [ + { + "type": 0, + "value": "链接" + } + ], "label.links": [ { "type": 0, "value": "链接" } ], + "label.location": [ + { + "type": 0, + "value": "位置" + } + ], "label.login": [ { "type": 0, @@ -1020,7 +1140,7 @@ "label.online": [ { "type": 0, - "value": "Online" + "value": "在线" } ], "label.organic-search": [ @@ -1165,6 +1285,12 @@ "value": "路径" } ], + "label.pixel": [ + { + "type": 0, + "value": "像素" + } + ], "label.pixels": [ { "type": 0, @@ -1185,6 +1311,12 @@ "value": " 提供支持" } ], + "label.preferences": [ + { + "type": 0, + "value": "偏好" + } + ], "label.previous": [ { "type": 0, @@ -1209,6 +1341,12 @@ "value": "个人资料" } ], + "label.profiles": [ + { + "type": 0, + "value": "个人资料" + } + ], "label.properties": [ { "type": 0, @@ -1248,7 +1386,7 @@ "label.referral": [ { "type": 0, - "value": "Referral" + "value": "来源" } ], "label.referrer": [ @@ -1371,6 +1509,24 @@ "value": "保存" } ], + "label.save-cohort": [ + { + "type": 0, + "value": "Save as cohort" + } + ], + "label.save-segment": [ + { + "type": 0, + "value": "Save as segment" + } + ], + "label.screen": [ + { + "type": 0, + "value": "屏幕" + } + ], "label.screens": [ { "type": 0, @@ -1383,6 +1539,18 @@ "value": "搜索" } ], + "label.segment": [ + { + "type": 0, + "value": "细分" + } + ], + "label.segments": [ + { + "type": 0, + "value": "细分" + } + ], "label.select": [ { "type": 0, @@ -1485,6 +1653,24 @@ "value": "总和" } ], + "label.support": [ + { + "type": 0, + "value": "支持" + } + ], + "label.switch-account": [ + { + "type": 0, + "value": "切换账户" + } + ], + "label.table": [ + { + "type": 0, + "value": "平板" + } + ], "label.tablet": [ { "type": 0, @@ -1635,6 +1821,12 @@ "value": "跟踪代码" } ], + "label.traffic": [ + { + "type": 0, + "value": "流量" + } + ], "label.transactions": [ { "type": 0, @@ -1846,7 +2038,7 @@ "message.bad-request": [ { "type": 0, - "value": "Bad request" + "value": "请求错误" } ], "message.collected-data": [ @@ -1946,7 +2138,7 @@ "message.forbidden": [ { "type": 0, - "value": "Forbidden" + "value": "禁止访问" } ], "message.go-to-settings": [ @@ -2046,13 +2238,13 @@ "message.not-found": [ { "type": 0, - "value": "Not found" + "value": "未找到" } ], "message.nothing-selected": [ { "type": 0, - "value": "Nothing selected." + "value": "未选择" } ], "message.page-not-found": [ @@ -2090,7 +2282,7 @@ "message.sever-error": [ { "type": 0, - "value": "Server error" + "value": "服务器错误" } ], "message.share-url": [ @@ -2158,7 +2350,7 @@ "message.unauthorized": [ { "type": 0, - "value": "Unauthorized" + "value": "未授权" } ], "message.user-deleted": [ diff --git a/src/lang/zh-CN.json b/src/lang/zh-CN.json index c6f01dd5..8fc0ea67 100644 --- a/src/lang/zh-CN.json +++ b/src/lang/zh-CN.json @@ -1,11 +1,15 @@ { "label.access-code": "访问代码", + "label.account": "账户", + "label.action": "行为", "label.actions": "用户行为", "label.activity": "活动日志", "label.add": "添加", "label.add-board": "添加看板", "label.add-description": "添加描述", + "label.add-link": "添加链接", "label.add-member": "添加成员", + "label.add-pixel": "添加像素", "label.add-step": "添加步骤", "label.add-website": "添加网站", "label.admin": "管理员", @@ -13,10 +17,13 @@ "label.after": "之后", "label.all": "所有", "label.all-time": "所有时间段", + "label.analysis": "分析", "label.analytics": "分析", + "label.application": "应用", "label.apply": "应用", "label.attribution": "归因", "label.attribution-description": "查看用户如何与您的营销互动,以及是什么促成了转化。", + "label.audience": "受众", "label.average": "平均", "label.back": "返回", "label.before": "之前", @@ -29,11 +36,14 @@ "label.campaigns": "活动", "label.cancel": "取消", "label.change-password": "修改密码", + "label.channel": "渠道", "label.channels": "渠道", + "label.chart": "图表", "label.cities": "市/县", "label.city": "市/县", "label.clear-all": "清除全部", "label.cohort": "队列", + "label.cohorts": "队列", "label.compare": "比较", "label.compare-dates": "比较日期", "label.confirm": "确认", @@ -53,6 +63,7 @@ "label.create-user": "创建用户", "label.created": "已创建", "label.created-by": "创建者", + "label.criteria": "条件", "label.currency": "货币", "label.current": "当前", "label.current-password": "当前密码", @@ -70,24 +81,28 @@ "label.delete-website": "删除网站", "label.description": "描述", "label.desktop": "台式机", + "label.destination-url": "目标URL", "label.details": "详细信息", "label.device": "设备", "label.devices": "设备", "label.direct": "直接", "label.dismiss": "关闭", "label.distinct-id": "唯一ID", + "label.documentation": "文档", "label.does-not-contain": "不包含", "label.does-not-include": "不包括", "label.doest-not-exist": "不存在", "label.domain": "域名", + "label.download": "下载", "label.dropoff": "丢弃", "label.edit": "编辑", "label.edit-dashboard": "编辑仪表盘", "label.edit-member": "编辑成员", - "label.email": "Email", + "label.email": "邮箱", "label.enable-share-url": "启用共享链接", "label.end-step": "结束步骤", "label.entry": "入口 URL", + "label.environment": "环境", "label.event": "事件", "label.event-data": "事件数据", "label.event-name": "事件名称", @@ -112,11 +127,13 @@ "label.greater-than": "大于", "label.greater-than-equals": "大于或等于", "label.grouped": "分组", + "label.growth": "Growth", "label.hostname": "主机名", "label.includes": "包括", "label.insight": "洞察", "label.insights": "见解", "label.insights-description": "通过使用筛选器和划分时间段来更深入地研究数据。", + "label.invalid-url": "无效URL", "label.is": "等于", "label.is-false": "否", "label.is-not": "不等于", @@ -140,7 +157,9 @@ "label.leave-team": "离开团队", "label.less-than": "少于", "label.less-than-equals": "少于等于", + "label.link": "链接", "label.links": "链接", + "label.location": "位置", "label.login": "登录", "label.logout": "退出", "label.manage": "管理", @@ -161,7 +180,7 @@ "label.none": "无", "label.number-of-records": "{x} {x, plural, one {record} other {records}}", "label.ok": "好的", - "label.online": "Online", + "label.online": "在线", "label.organic-search": "自然搜索", "label.organic-shopping": "自然购物", "label.organic-social": "自然社交", @@ -183,19 +202,22 @@ "label.password": "密码", "label.path": "路径", "label.paths": "路径", + "label.pixel": "像素", "label.pixels": "像素", "label.powered-by": "由 {name} 提供支持", + "label.preferences": "偏好", "label.previous": "先前", "label.previous-period": "上一时期", "label.previous-year": "上一年", "label.profile": "个人资料", + "label.profiles": "个人资料", "label.properties": "属性", "label.property": "属性", "label.queries": "查询", "label.query": "查询", "label.query-parameters": "查询参数", "label.realtime": "实时", - "label.referral": "Referral", + "label.referral": "来源", "label.referrer": "来源", "label.referrers": "来源域名", "label.refresh": "刷新", @@ -216,8 +238,13 @@ "label.role": "角色", "label.run-query": "查询", "label.save": "保存", + "label.save-cohort": "Save as cohort", + "label.save-segment": "Save as segment", + "label.screen": "屏幕", "label.screens": "屏幕尺寸", "label.search": "搜索", + "label.segment": "细分", + "label.segments": "细分", "label.select": "选择", "label.select-date": "选择日期", "label.select-filter": "选择筛选器", @@ -235,6 +262,9 @@ "label.start-step": "开始步骤", "label.steps": "步骤", "label.sum": "总和", + "label.support": "支持", + "label.switch-account": "切换账户", + "label.table": "平板", "label.tablet": "平板", "label.tag": "标签", "label.tags": "标签", @@ -260,6 +290,7 @@ "label.total": "总数", "label.total-records": "总记录数", "label.tracking-code": "跟踪代码", + "label.traffic": "流量", "label.transactions": "交易", "label.transfer": "转移", "label.transfer-website": "转移网站", @@ -292,7 +323,7 @@ "label.yesterday": "昨天", "message.action-confirmation": "请在下方输入框中输入 {confirmation} 以确认操作。", "message.active-users": "当前在线 {x} 位访客", - "message.bad-request": "Bad request", + "message.bad-request": "请求错误", "message.collected-data": "已收集的数据", "message.confirm-delete": "你确定要删除 {target} 吗?", "message.confirm-leave": "你确定要离开 {target} 吗?", @@ -302,7 +333,7 @@ "message.delete-website-warning": "所有相关数据将会被删除。", "message.error": "发生错误。", "message.event-log": "{url} 上的 {event}", - "message.forbidden": "Forbidden", + "message.forbidden": "禁止访问", "message.go-to-settings": "去设置", "message.incorrect-username-password": "用户名或密码不正确。", "message.invalid-domain": "无效域名", @@ -316,13 +347,13 @@ "message.no-teams": "您尚未创建任何团队。", "message.no-users": "暂无用户。", "message.no-websites-configured": "你还没有设置任何网站。", - "message.not-found": "Not found", - "message.nothing-selected": "Nothing selected.", + "message.not-found": "未找到", + "message.nothing-selected": "未选择", "message.page-not-found": "页面未找到。", "message.reset-website": "如确定要重置该网站,请在下面输入 {confirmation} 以确认。", "message.reset-website-warning": "此网站的所有统计数据将被删除,但您的跟踪代码将保持不变。", "message.saved": "保存成功。", - "message.sever-error": "Server error", + "message.sever-error": "服务器错误", "message.share-url": "这是 {target} 的共享链接。", "message.team-already-member": "你已是该团队的成员。", "message.team-not-found": "未找到团队。", @@ -332,7 +363,7 @@ "message.transfer-user-website-to-team": "选择要转移此网站的团队。", "message.transfer-website": "将网站所有权转移到您的账户或其他团队。", "message.triggered-event": "触发事件", - "message.unauthorized": "Unauthorized", + "message.unauthorized": "未授权", "message.user-deleted": "用户已删除。", "message.viewed-page": "已浏览页面", "message.visitor-log": "来自 {country} 的访客在搭载 {os} 的 {device} 上使用 {browser} 浏览器进行访问。" From 783098fadcfa32cbdb62bcfaf48fbe317ccbe0fc Mon Sep 17 00:00:00 2001 From: JiPai Date: Tue, 23 Dec 2025 23:04:41 +0800 Subject: [PATCH 10/13] chore(i18n): update zh-CN translation --- public/intl/messages/zh-CN.json | 8 ++++---- src/lang/zh-CN.json | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/public/intl/messages/zh-CN.json b/public/intl/messages/zh-CN.json index 1c3f3240..a4ad51fa 100644 --- a/public/intl/messages/zh-CN.json +++ b/public/intl/messages/zh-CN.json @@ -770,7 +770,7 @@ "label.growth": [ { "type": 0, - "value": "Growth" + "value": "增长" } ], "label.hostname": [ @@ -1512,13 +1512,13 @@ "label.save-cohort": [ { "type": 0, - "value": "Save as cohort" + "value": "保存为群组" } ], "label.save-segment": [ { "type": 0, - "value": "Save as segment" + "value": "保存为细分" } ], "label.screen": [ @@ -1668,7 +1668,7 @@ "label.table": [ { "type": 0, - "value": "平板" + "value": "表格" } ], "label.tablet": [ diff --git a/src/lang/zh-CN.json b/src/lang/zh-CN.json index 8fc0ea67..5490df42 100644 --- a/src/lang/zh-CN.json +++ b/src/lang/zh-CN.json @@ -127,7 +127,7 @@ "label.greater-than": "大于", "label.greater-than-equals": "大于或等于", "label.grouped": "分组", - "label.growth": "Growth", + "label.growth": "增长", "label.hostname": "主机名", "label.includes": "包括", "label.insight": "洞察", @@ -238,8 +238,8 @@ "label.role": "角色", "label.run-query": "查询", "label.save": "保存", - "label.save-cohort": "Save as cohort", - "label.save-segment": "Save as segment", + "label.save-cohort": "保存为群组", + "label.save-segment": "保存为细分", "label.screen": "屏幕", "label.screens": "屏幕尺寸", "label.search": "搜索", @@ -264,7 +264,7 @@ "label.sum": "总和", "label.support": "支持", "label.switch-account": "切换账户", - "label.table": "平板", + "label.table": "表格", "label.tablet": "平板", "label.tag": "标签", "label.tags": "标签", From 4eddac21c754d900c0db71a8f8c3745d0d3cb7db Mon Sep 17 00:00:00 2001 From: Yash Date: Thu, 25 Dec 2025 20:41:14 +0530 Subject: [PATCH 11/13] feat: Add default currency support and update currency handling in Revenue component --- next.config.ts | 2 ++ .../[websiteId]/(reports)/revenue/Revenue.tsx | 15 ++++++++++++--- src/lib/constants.ts | 2 ++ src/lib/format.ts | 4 +++- 4 files changed, 19 insertions(+), 4 deletions(-) diff --git a/next.config.ts b/next.config.ts index 99dcca0d..1a4e2e0e 100644 --- a/next.config.ts +++ b/next.config.ts @@ -8,6 +8,7 @@ const cloudMode = process.env.CLOUD_MODE || ''; const cloudUrl = process.env.CLOUD_URL || ''; const collectApiEndpoint = process.env.COLLECT_API_ENDPOINT || ''; const corsMaxAge = process.env.CORS_MAX_AGE || ''; +const defaultCurrency = process.env.DEFAULT_CURRENCY || ''; const defaultLocale = process.env.DEFAULT_LOCALE || ''; const forceSSL = process.env.FORCE_SSL || ''; const frameAncestors = process.env.ALLOWED_FRAME_URLS || ''; @@ -170,6 +171,7 @@ export default { cloudMode, cloudUrl, currentVersion: pkg.version, + defaultCurrency, defaultLocale, }, basePath, diff --git a/src/app/(main)/websites/[websiteId]/(reports)/revenue/Revenue.tsx b/src/app/(main)/websites/[websiteId]/(reports)/revenue/Revenue.tsx index 0e782a16..faee8b9a 100644 --- a/src/app/(main)/websites/[websiteId]/(reports)/revenue/Revenue.tsx +++ b/src/app/(main)/websites/[websiteId]/(reports)/revenue/Revenue.tsx @@ -12,9 +12,10 @@ import { ListTable } from '@/components/metrics/ListTable'; import { MetricCard } from '@/components/metrics/MetricCard'; import { MetricsBar } from '@/components/metrics/MetricsBar'; import { renderDateLabels } from '@/lib/charts'; -import { CHART_COLORS } from '@/lib/constants'; +import { CHART_COLORS, CURRENCY_CONFIG, DEFAULT_CURRENCY } from '@/lib/constants'; import { generateTimeSeries } from '@/lib/date'; import { formatLongCurrency, formatLongNumber } from '@/lib/format'; +import { getItem, setItem } from '@/lib/storage'; export interface RevenueProps { websiteId: string; @@ -24,7 +25,15 @@ export interface RevenueProps { } export function Revenue({ websiteId, startDate, endDate, unit }: RevenueProps) { - const [currency, setCurrency] = useState('USD'); + const [currency, setCurrency] = useState( + getItem(CURRENCY_CONFIG) || process.env.defaultCurrency || DEFAULT_CURRENCY, + ); + + const handleCurrencyChange = (value: string) => { + setCurrency(value); + setItem(CURRENCY_CONFIG, value); + }; + const { formatMessage, labels } = useMessages(); const { locale, dateLocale } = useLocale(); const { countryNames } = useCountryNames(locale); @@ -107,7 +116,7 @@ export function Revenue({ websiteId, startDate, endDate, unit }: RevenueProps) { return ( - + {data && ( diff --git a/src/lib/constants.ts b/src/lib/constants.ts index e5090c3c..502a3df6 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -4,6 +4,7 @@ export const LOCALE_CONFIG = 'umami.locale'; export const TIMEZONE_CONFIG = 'umami.timezone'; export const DATE_RANGE_CONFIG = 'umami.date-range'; export const THEME_CONFIG = 'umami.theme'; +export const CURRENCY_CONFIG = 'umami.currency'; export const DASHBOARD_CONFIG = 'umami.dashboard'; export const LAST_TEAM_CONFIG = 'umami.last-team'; export const VERSION_CHECK = 'umami.version-check'; @@ -25,6 +26,7 @@ export const DEFAULT_WEBSITE_LIMIT = 10; export const DEFAULT_RESET_DATE = '2000-01-01'; export const DEFAULT_PAGE_SIZE = 20; export const DEFAULT_DATE_COMPARE = 'prev'; +export const DEFAULT_CURRENCY = 'USD'; export const REALTIME_RANGE = 30; export const REALTIME_INTERVAL = 10000; diff --git a/src/lib/format.ts b/src/lib/format.ts index 52fd3048..035a1811 100644 --- a/src/lib/format.ts +++ b/src/lib/format.ts @@ -1,3 +1,5 @@ +import { DEFAULT_CURRENCY } from './constants'; + export function parseTime(val: number) { const days = ~~(val / 86400); const hours = ~~(val / 3600) - days * 24; @@ -94,7 +96,7 @@ export function formatCurrency(value: number, currency: string, locale = 'en-US' // Fallback to default currency format if an error occurs formattedValue = new Intl.NumberFormat(locale, { style: 'currency', - currency: 'USD', + currency: DEFAULT_CURRENCY, }); } From 97c26bc0759caabdde17d0e35c829bb0b0e424d6 Mon Sep 17 00:00:00 2001 From: Dimitar Yanakiev Date: Fri, 26 Dec 2025 16:34:38 +0200 Subject: [PATCH 12/13] Translated various labels and messages in Bulgarian. --- src/lang/bg-BG.json | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/src/lang/bg-BG.json b/src/lang/bg-BG.json index 4b0effc8..50099032 100644 --- a/src/lang/bg-BG.json +++ b/src/lang/bg-BG.json @@ -93,7 +93,7 @@ "label.event-name": "Име на събитие", "label.events": "Събития", "label.exists": "Съществува", - "label.exit": "Exit URL", + "label.exit": "URL за изход", "label.false": "Грешно", "label.field": "Поле", "label.fields": "Полета", @@ -135,7 +135,7 @@ "label.last-days": "Последните {x} дни", "label.last-hours": "Последните {x} часа", "label.last-months": "Последните {x} месеца", - "label.last-seen": "Last seen", + "label.last-seen": "Последно видяно", "label.leave": "Напусни", "label.leave-team": "Напусни екип", "label.less-than": "По-малко от", @@ -161,7 +161,7 @@ "label.none": "Няма", "label.number-of-records": "{x} {x, plural, one {един} other {други}}", "label.ok": "Добре", - "label.online": "Online", + "label.online": "Онлайн", "label.organic-search": "Органично търсене", "label.organic-shopping": "Органично пазаруване", "label.organic-social": "Органични социални мрежи", @@ -185,9 +185,9 @@ "label.paths": "Пътища", "label.pixels": "Пиксели", "label.powered-by": "Поддържано от {name}", - "label.previous": "Previous", - "label.previous-period": "Previous period", - "label.previous-year": "Previous year", + "label.previous": "Предишен", + "label.previous-period": "Предишен период", + "label.previous-year": "Предишна година", "label.profile": "Профил", "label.properties": "Свойства", "label.property": "Свойство", @@ -211,8 +211,8 @@ "label.reset-website": "Нулирай уебсайт", "label.retention": "Привързване", "label.retention-description": "Измерете привързаността към вашия уебсайт, като проследявате колко често потребителите се връщат.", - "label.revenue": "Revenue", - "label.revenue-description": "Look into your revenue across time.", + "label.revenue": "Приходи", + "label.revenue-description": "Прегледайте приходите си във времето.", "label.role": "Роля", "label.run-query": "Изпълни запитване", "label.save": "Запази", @@ -260,14 +260,14 @@ "label.total": "Общо", "label.total-records": "Общо записи", "label.tracking-code": "Код за проследяване", - "label.transactions": "Transactions", + "label.transactions": "Транзакции", "label.transfer": "Прехвърли", "label.transfer-website": "Прехвърляне на уебсайт", "label.true": "Вярно", "label.type": "Вид", "label.unique": "Уникален", "label.unique-visitors": "Уникални посетители", - "label.uniqueCustomers": "Unique Customers", + "label.uniqueCustomers": "Уникални клиенти", "label.unknown": "Неизвестен", "label.untitled": "Без заглавие", "label.update": "Актуализирай", @@ -282,7 +282,7 @@ "label.view-only": "Само за преглед", "label.views": "Прегледи", "label.views-per-visit": "Прегледи на посещение", - "label.visit-duration": "Visit duration", + "label.visit-duration": "Продължителност на посещение", "label.visitors": "Посетители", "label.visits": "Посещения", "label.website": "Уебсайт", @@ -292,8 +292,8 @@ "label.yesterday": "Вчера", "message.action-confirmation": "Въведете {confirmation} в полето по-долу, за да потвърдите.", "message.active-users": "{x} {x, plural, one {активен един} other {активни други}}", - "message.bad-request": "Bad request", - "message.collected-data": "Collected data", + "message.bad-request": "Невалидна заявка", + "message.collected-data": "Събрани данни", "message.confirm-delete": "Сигурни ли сте, че искате да изтриете {target}?", "message.confirm-leave": "Сигурни ли сте, че искате да напуснете {target}?", "message.confirm-remove": "Сигурни ли сте, че искате да премахнете {target}?", @@ -302,7 +302,7 @@ "message.delete-website-warning": "Всички данни за уебсайта ще бъдат изтрити.", "message.error": "Възникна грешка.", "message.event-log": "{event} на {url}", - "message.forbidden": "Forbidden", + "message.forbidden": "Забранено", "message.go-to-settings": "Отидете в настройките", "message.incorrect-username-password": "Неправилно потребителско име и/или парола.", "message.invalid-domain": "Невалиден домейн. Не включвайте http/https.", @@ -316,13 +316,13 @@ "message.no-teams": "Няма създадени екипи.", "message.no-users": "Няма потребители.", "message.no-websites-configured": "Нямате конфигурирани уебсайтове.", - "message.not-found": "Not found", - "message.nothing-selected": "Nothing selected.", + "message.not-found": "Не е намерено", + "message.nothing-selected": "Няма избрано.", "message.page-not-found": "Страницата не е намерена", "message.reset-website": "За да нулирате този уебсайт, въведете {confirmation} в полето по-долу, за да потвърдите.", "message.reset-website-warning": "Всички статистически данни за този уебсайт ще бъдат изтрити, но вашите настройки ще останат непроменени.", "message.saved": "Запазено.", - "message.sever-error": "Server error", + "message.sever-error": "Сървърна грешка", "message.share-url": "Статистиката за вашия уебсайт е публично достъпна на следния URL адрес:", "message.team-already-member": "Вече сте член на екипа.", "message.team-not-found": "Екипът не е намерен.", @@ -332,7 +332,7 @@ "message.transfer-user-website-to-team": "Изберете екипът на който да бъде прехвърлен уебсайта.", "message.transfer-website": "Прехвърли собствеността на уебсайта към вашия акаунт или към друг екип.", "message.triggered-event": "Активирано събитие", - "message.unauthorized": "Unauthorized", + "message.unauthorized": "Неоторизиран достъп", "message.user-deleted": "Потребителят е изтрит.", "message.viewed-page": "Страницата е видяна", "message.visitor-log": "Посетител от {country}, използващ {browser} на {os} {device}" From 491716f4dd9b7d524ab7b28728a5fd40ee0274e9 Mon Sep 17 00:00:00 2001 From: AymanAlSuleihi Date: Sat, 3 Jan 2026 00:48:09 +0000 Subject: [PATCH 13/13] Fix double scrollbar in dropdown --- src/components/input/WebsiteSelect.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/input/WebsiteSelect.tsx b/src/components/input/WebsiteSelect.tsx index 8d81eb9a..04c773a7 100644 --- a/src/components/input/WebsiteSelect.tsx +++ b/src/components/input/WebsiteSelect.tsx @@ -65,7 +65,7 @@ export function WebsiteSelect({ renderValue={renderValue} listProps={{ renderEmptyState: () => , - style: { maxHeight: '400px' }, + style: { maxHeight: 'calc(42vh - 65px)' }, }} > {({ id, name }: any) => {name}}