Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
202 changes: 145 additions & 57 deletions app/[lang]/_components/LocalizedLink.tsx
Original file line number Diff line number Diff line change
@@ -1,74 +1,162 @@
'use client';
'use client'

import Link from 'next/link';
import {usePathname} from 'next/navigation';
import {forwardRef} from 'react';
import Link from 'next/link'
import {usePathname} from 'next/navigation'
import {forwardRef} from 'react'

import {DEFAULT_LANGUAGE, getLanguageFromPath} from '@/app/[lang]/_utils/i18nconfig';
import {DEFAULT_LANGUAGE, getLanguageFromPath} from '@/app/[lang]/_utils/i18nconfig'

import type {LinkProps} from 'next/link';
import type {AnchorHTMLAttributes, ReactNode} from 'react';
import type {UrlObject} from 'url';
import type {LinkProps} from 'next/link'
import type {AnchorHTMLAttributes, ReactNode} from 'react'
import type {UrlObject} from 'url'

/* eslint-disable @typescript-eslint/naming-convention */
declare const __adrsbl: {run: (event: string, conversion: boolean) => void} | undefined

type TLocalizedLinkProps = LinkProps &
AnchorHTMLAttributes<HTMLAnchorElement> & {
children: ReactNode;
};
children: ReactNode
}

/**
* A localized version of Next.js Link that automatically prepends the current language
* to internal links when needed.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This line is still removed?

*/
export const LocalizedLink = forwardRef<HTMLAnchorElement, TLocalizedLinkProps>(({href, children, ...props}, ref) => {
const pathname = usePathname();
const currentLanguage = getLanguageFromPath(pathname) || DEFAULT_LANGUAGE;

// Convert href to string for processing
const hrefString =
typeof href === 'string'
? href
: typeof href === 'object' &&
href !== null &&
'pathname' in href &&
typeof (href as UrlObject).pathname === 'string'
? (href as UrlObject).pathname
: '';

// Don't modify external links, anchors, or already localized paths
if (
hrefString?.startsWith('http') ||
hrefString?.startsWith('#') ||
hrefString?.startsWith('mailto:') ||
hrefString?.startsWith('tel:') ||
!hrefString?.startsWith('/')
) {
export const LocalizedLink = forwardRef<HTMLAnchorElement, TLocalizedLinkProps>(
({href, children, onClick, ...props}, ref) => {
const pathname = usePathname()
const currentLanguage = getLanguageFromPath(pathname) || DEFAULT_LANGUAGE

// Preserve UrlObject hrefs (pathname + query + hash) when provided
const hrefObj = typeof href === 'object' && href !== null ? (href as UrlObject) : undefined

// Convert href to string for pathname-based processing when needed
const hrefString = typeof href === 'string' ? href : (hrefObj?.pathname ?? '')

// Helper to build a full absolute URL string from a UrlObject when it has host/protocol
const fullUrlFromObj = (obj: UrlObject) => {
if (!obj) {
return ''
}
const protocol = (obj.protocol as string) ?? ''
const host = (obj.host as string) ?? (obj.hostname as string) ?? ''
const pathname = (obj.pathname as string) ?? ''
const hash = (obj.hash as string) ?? ''
let query = ''
// Support both string-form and object-form queries.
if (obj.query) {
if (typeof obj.query === 'string') {
// Use provided string query, ensure it starts with '?'
const raw = obj.query as string
query = raw.startsWith('?') ? raw : `?${raw}`
} else if (typeof obj.query === 'object') {
// obj.query can be Record<string, string | string[]>.
const params = new URLSearchParams()
for (const [k, v] of Object.entries(obj.query as Record<string, string | string[]>)) {
if (Array.isArray(v)) {
for (const item of v) {
params.append(k, String(item))
}
} else if (v !== undefined && v !== null) {
params.append(k, String(v))
}
}
const qs = params.toString()
query = qs ? `?${qs}` : ''
}
}
if (host) {
return `${protocol || 'https:'}//${host}${pathname}${query}${hash}`
}
return `${pathname}${query}${hash}`
}

// External app.shapeshift.com link detection (works for string hrefs and UrlObject with host)
const isAppLink = (() => {
if (typeof href === 'string') {
return /^https?:\/\/app\.shapeshift\.com(\/|$)/i.test(href)
}
if (hrefObj) {
const full = fullUrlFromObj(hrefObj)
return /^https?:\/\/app\.shapeshift\.com(\/|$)/i.test(full)
}
return false
})()

// Compose click handler for external app links
const handleClick = (e: React.MouseEvent<HTMLAnchorElement>) => {
if (isAppLink) {
try {
if (__adrsbl?.run) {
__adrsbl.run('app_click', true)
}
} catch {
// ignore
}
e.preventDefault()
const absolute = hrefObj
? fullUrlFromObj(hrefObj)
: hrefString || (typeof href === 'string' ? href : '')
try {
window.open(absolute, '_blank', 'noopener,noreferrer')
} catch {
if (absolute) {
window.location.assign(absolute)
}
}
}
if (onClick) {
onClick(e)
}
}

// Don't modify external links, anchors, or already localized paths
const isExternalObject = !!hrefObj && !!(hrefObj.host || hrefObj.hostname || hrefObj.protocol)
if (
typeof href === 'string'
? hrefString.startsWith('http') ||
hrefString.startsWith('#') ||
hrefString.startsWith('mailto:') ||
hrefString.startsWith('tel:') ||
!hrefString.startsWith('/')
: // href is an object
isExternalObject
) {
return (
<Link
href={href}
ref={ref}
{...props}
onClick={isAppLink ? handleClick : onClick}>
{children}
</Link>
)
}

// Check if the href already has a language prefix
const hasLanguagePrefix = hrefString.match(/^\/([a-z]{2})(\/|$)/)

// Build the localized href. Preserve query/hash by returning a UrlObject when the original
// href was a UrlObject, otherwise return a string.
let localizedPathname = hrefString
if (!hasLanguagePrefix && currentLanguage !== DEFAULT_LANGUAGE) {
localizedPathname = `/${currentLanguage}${hrefString}`
}

const localizedHref = hrefObj
? // copy the original UrlObject but replace/normalize pathname
({...hrefObj, pathname: localizedPathname} as UrlObject)
: localizedPathname

return (
<Link
href={href}
href={localizedHref}
ref={ref}
{...props}>
{...props}
onClick={onClick}>
{children}
</Link>
);
}

// Check if the href already has a language prefix
const hasLanguagePrefix = hrefString.match(/^\/[a-z]{2}(\/|$)/);

// Build the localized href
let localizedHref = hrefString;
if (!hasLanguagePrefix && currentLanguage !== DEFAULT_LANGUAGE) {
localizedHref = `/${currentLanguage}${hrefString}`;
)
}
)

return (
<Link
href={localizedHref}
ref={ref}
{...props}>
{children}
</Link>
);
});

LocalizedLink.displayName = 'LocalizedLink';
LocalizedLink.displayName = 'LocalizedLink'
2 changes: 1 addition & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,5 +33,5 @@
"eslint.config.mjs",
"postcss.config.mjs"
],
"exclude": ["node_modules", ".next"]
"exclude": ["node_modules", ".next", "scripts"]
}