diff --git a/package.json b/package.json index e9a286bca..0d701db8b 100644 --- a/package.json +++ b/package.json @@ -100,6 +100,7 @@ "pg": "^8.22.0", "prisma": "^7.8.0", "prop-types": "^15.8.1", + "punycode": "^2.3.1", "react": "^19.2.7", "react-aria-components": "^1.19.0", "react-dom": "^19.2.7", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a52e43b2e..da88a5ce2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -149,6 +149,9 @@ importers: prop-types: specifier: ^15.8.1 version: 15.8.1 + punycode: + specifier: ^2.3.1 + version: 2.3.1 react: specifier: ^19.2.7 version: 19.2.7 diff --git a/src/app/(main)/admin/websites/AdminWebsitesTable.tsx b/src/app/(main)/admin/websites/AdminWebsitesTable.tsx index 12698e730..f33b4c35d 100644 --- a/src/app/(main)/admin/websites/AdminWebsitesTable.tsx +++ b/src/app/(main)/admin/websites/AdminWebsitesTable.tsx @@ -7,6 +7,7 @@ import { SortableLabel } from '@/components/common/SortableLabel'; import { useMessages } from '@/components/hooks'; import { Edit, Trash, Users } from '@/components/icons'; import { MenuButton } from '@/components/input/MenuButton'; +import { decodePunycodeDomain } from '@/lib/format'; export function AdminWebsitesTable({ data = [], ...props }: { data: any[] }) { const { t, labels } = useMessages(); @@ -23,7 +24,7 @@ export function AdminWebsitesTable({ data = [], ...props }: { data: any[] }) { )} }> - {(row: any) => {row.domain}} + {(row: any) => {decodePunycodeDomain(row.domain)}} {(row: any) => { diff --git a/src/app/(main)/websites/WebsitesTable.tsx b/src/app/(main)/websites/WebsitesTable.tsx index e6716f9e4..4b3e13ce3 100644 --- a/src/app/(main)/websites/WebsitesTable.tsx +++ b/src/app/(main)/websites/WebsitesTable.tsx @@ -5,6 +5,7 @@ import { LinkButton } from '@/components/common/LinkButton'; import { SortableLabel } from '@/components/common/SortableLabel'; import { useMessages, useNavigation, useWebsiteListChartsQuery } from '@/components/hooks'; import { SquarePen } from '@/components/icons'; +import { decodePunycodeDomain } from '@/lib/format'; import { WebsiteSparkline } from './WebsiteSparkline'; export interface WebsitesTableProps extends DataTableProps { @@ -42,8 +43,12 @@ export function WebsitesTable({ style={{ minWidth: 0 }} > {(row: any) => ( - - {row.domain} + + {decodePunycodeDomain(row.domain)} )} diff --git a/src/app/(main)/websites/[websiteId]/settings/WebsiteSettingsHeader.tsx b/src/app/(main)/websites/[websiteId]/settings/WebsiteSettingsHeader.tsx index b96d3c6e7..16a81edff 100644 --- a/src/app/(main)/websites/[websiteId]/settings/WebsiteSettingsHeader.tsx +++ b/src/app/(main)/websites/[websiteId]/settings/WebsiteSettingsHeader.tsx @@ -4,6 +4,7 @@ import Link from '@/components/common/Link'; import { PageHeader } from '@/components/common/PageHeader'; import { useMessages, useNavigation, useWebsite } from '@/components/hooks'; import { ArrowLeft, Globe } from '@/components/icons'; +import { decodePunycodeDomain } from '@/lib/format'; export function WebsiteSettingsHeader() { const website = useWebsite(); @@ -17,7 +18,11 @@ export function WebsiteSettingsHeader() { } label={t(labels.website)} /> - } /> + } + /> ); } diff --git a/src/components/metrics/MetricLabel.tsx b/src/components/metrics/MetricLabel.tsx index 21618b90d..c0b9857d5 100644 --- a/src/components/metrics/MetricLabel.tsx +++ b/src/components/metrics/MetricLabel.tsx @@ -10,6 +10,7 @@ import { useRegionNames, } from '@/components/hooks'; import { GROUPED_DOMAINS } from '@/lib/constants'; +import { decodePunycodeDomain } from '@/lib/format'; export interface MetricLabelProps { type: string; @@ -113,7 +114,7 @@ export function MetricLabel({ type, data }: MetricLabelProps) { type="referrer" value={label} externalUrl={`https://${label}`} - label={!label && t(labels.none)} + label={!label ? t(labels.none) : decodePunycodeDomain(label)} icon={} /> ); diff --git a/src/lib/format.test.ts b/src/lib/format.test.ts index 4f9b192a2..b70b5f2e1 100644 --- a/src/lib/format.test.ts +++ b/src/lib/format.test.ts @@ -42,3 +42,9 @@ test('truncateString', () => { expect(format.truncateString('hello', 3)).toBe('hel'); expect(format.truncateString(undefined, 3)).toBeUndefined(); }); + +test('decodePunycodeDomain', () => { + expect(format.decodePunycodeDomain('xn--bcher-kva.example')).toBe('bücher.example'); + expect(format.decodePunycodeDomain('example.com')).toBe('example.com'); + expect(format.decodePunycodeDomain(undefined)).toBeUndefined(); +}); diff --git a/src/lib/format.ts b/src/lib/format.ts index a92fb4423..07110d935 100644 --- a/src/lib/format.ts +++ b/src/lib/format.ts @@ -1,3 +1,4 @@ +import { toUnicode } from 'punycode'; import { DEFAULT_CURRENCY } from './constants'; export function parseTime(val: number) { @@ -119,6 +120,18 @@ export function formatLongCurrency(value: number, currency: string, locale = 'en return formatCurrency(n, currency, locale); } +export function decodePunycodeDomain(domain?: string | null) { + if (!domain) { + return domain; + } + + try { + return toUnicode(domain); + } catch { + return domain; + } +} + export function truncateString( value: T, maxLength: number,