@@ -12,31 +12,77 @@ import type {UrlObject} from 'url'
12
12
13
13
/* eslint-disable @typescript-eslint/naming-convention */
14
14
declare const __adrsbl : { run : ( event : string , conversion : boolean ) => void } | undefined
15
- /* eslint-enable @typescript-eslint/naming-convention */
16
15
17
16
type TLocalizedLinkProps = LinkProps &
18
17
AnchorHTMLAttributes < HTMLAnchorElement > & {
19
18
children : ReactNode
20
19
}
21
20
21
+ /**
22
+ * A localized version of Next.js Link that automatically prepends the current language
23
+ */
22
24
export const LocalizedLink = forwardRef < HTMLAnchorElement , TLocalizedLinkProps > (
23
25
( { href, children, onClick, ...props } , ref ) => {
24
26
const pathname = usePathname ( )
25
27
const currentLanguage = getLanguageFromPath ( pathname ) || DEFAULT_LANGUAGE
26
28
27
- // Convert href to string for processing
28
- const hrefString =
29
- typeof href === 'string'
30
- ? href
31
- : typeof href === 'object' &&
32
- href !== null &&
33
- 'pathname' in href &&
34
- typeof ( href as UrlObject ) . pathname === 'string'
35
- ? ( href as UrlObject ) . pathname
36
- : ''
29
+ // Preserve UrlObject hrefs (pathname + query + hash) when provided
30
+ const hrefObj = typeof href === 'object' && href !== null ? ( href as UrlObject ) : undefined
37
31
38
- // External app.shapeshift.com link detection
39
- const isAppLink = typeof hrefString === 'string' && / ^ h t t p s ? : \/ \/ a p p \. s h a p e s h i f t \. c o m ( \/ | $ ) / i. test ( hrefString )
32
+ // Convert href to string for pathname-based processing when needed
33
+ const hrefString = typeof href === 'string' ? href : ( hrefObj ?. pathname ?? '' )
34
+
35
+ // Helper to build a full absolute URL string from a UrlObject when it has host/protocol
36
+ const fullUrlFromObj = ( obj : UrlObject ) => {
37
+ if ( ! obj ) {
38
+ return ''
39
+ }
40
+ const protocol = ( obj . protocol as string ) ?? ''
41
+ const host = ( obj . host as string ) ?? ( obj . hostname as string ) ?? ''
42
+ const pathname = ( obj . pathname as string ) ?? ''
43
+ const hash = ( obj . hash as string ) ?? ''
44
+ let query = ''
45
+ // Support both string-form and object-form queries.
46
+ if ( obj . query ) {
47
+ if ( typeof obj . query === 'string' ) {
48
+ // Use provided string query, ensure it starts with '?'
49
+ const raw = obj . query as string
50
+ query = raw . startsWith ( '?' ) ? raw : `?${ raw } `
51
+ } else if ( typeof obj . query === 'object' ) {
52
+ // obj.query can be Record<string, string | string[]>.
53
+ const params = new URLSearchParams ( )
54
+ for ( const [ k , v ] of Object . entries ( obj . query as Record < string , string | string [ ] > ) ) {
55
+ if ( Array . isArray ( v ) ) {
56
+ for ( const item of v ) {
57
+ params . append ( k , String ( item ) )
58
+ }
59
+ } else if ( v !== undefined && v !== null ) {
60
+ params . append ( k , String ( v ) )
61
+ } else {
62
+ params . append ( k , '' )
63
+ }
64
+ }
65
+ const qs = params . toString ( )
66
+ query = qs ? `?${ qs } ` : ''
67
+ }
68
+ }
69
+ if ( host ) {
70
+ return `${ protocol || 'https:' } //${ host } ${ pathname } ${ query } ${ hash } `
71
+ }
72
+ return `${ pathname } ${ query } ${ hash } `
73
+ }
74
+
75
+ // External app.shapeshift.com link detection (works for string hrefs and UrlObject with host)
76
+ const isAppLink = ( ( ) => {
77
+ if ( typeof href === 'string' ) {
78
+ return / ^ h t t p s ? : \/ \/ a p p \. s h a p e s h i f t \. c o m ( \/ | $ ) / i. test ( href )
79
+ }
80
+ if ( hrefObj ) {
81
+ const full = fullUrlFromObj ( hrefObj )
82
+ return / ^ h t t p s ? : \/ \/ a p p \. s h a p e s h i f t \. c o m ( \/ | $ ) / i. test ( full )
83
+ }
84
+ return false
85
+ } ) ( )
40
86
41
87
// Compose click handler for external app links
42
88
const handleClick = ( e : React . MouseEvent < HTMLAnchorElement > ) => {
@@ -50,10 +96,14 @@ export const LocalizedLink = forwardRef<HTMLAnchorElement, TLocalizedLinkProps>(
50
96
}
51
97
e . preventDefault ( )
52
98
try {
53
- window . open ( hrefString || ( typeof href === 'string' ? href : '' ) , '_blank' , 'noopener,noreferrer' )
99
+ const absolute = hrefObj
100
+ ? fullUrlFromObj ( hrefObj )
101
+ : hrefString || ( typeof href === 'string' ? href : '' )
102
+ window . open ( absolute , '_blank' , 'noopener,noreferrer' )
54
103
} catch {
55
- if ( hrefString ) {
56
- window . location . assign ( hrefString )
104
+ const absolute = hrefObj ? fullUrlFromObj ( hrefObj ) : hrefString
105
+ if ( absolute ) {
106
+ window . location . assign ( absolute )
57
107
}
58
108
}
59
109
}
@@ -63,12 +113,16 @@ export const LocalizedLink = forwardRef<HTMLAnchorElement, TLocalizedLinkProps>(
63
113
}
64
114
65
115
// Don't modify external links, anchors, or already localized paths
116
+ const isExternalObject = ! ! hrefObj && ! ! ( hrefObj . host || hrefObj . hostname || hrefObj . protocol )
66
117
if (
67
- hrefString ?. startsWith ( 'http' ) ||
68
- hrefString ?. startsWith ( '#' ) ||
69
- hrefString ?. startsWith ( 'mailto:' ) ||
70
- hrefString ?. startsWith ( 'tel:' ) ||
71
- ! hrefString ?. startsWith ( '/' )
118
+ typeof href === 'string'
119
+ ? hrefString . startsWith ( 'http' ) ||
120
+ hrefString . startsWith ( '#' ) ||
121
+ hrefString . startsWith ( 'mailto:' ) ||
122
+ hrefString . startsWith ( 'tel:' ) ||
123
+ ! hrefString . startsWith ( '/' )
124
+ : // href is an object
125
+ isExternalObject
72
126
) {
73
127
return (
74
128
< Link
@@ -84,12 +138,18 @@ export const LocalizedLink = forwardRef<HTMLAnchorElement, TLocalizedLinkProps>(
84
138
// Check if the href already has a language prefix
85
139
const hasLanguagePrefix = hrefString . match ( / ^ \/ ( [ a - z ] { 2 } ) ( \/ | $ ) / )
86
140
87
- // Build the localized href
88
- let localizedHref = hrefString
141
+ // Build the localized href. Preserve query/hash by returning a UrlObject when the original
142
+ // href was a UrlObject, otherwise return a string.
143
+ let localizedPathname = hrefString
89
144
if ( ! hasLanguagePrefix && currentLanguage !== DEFAULT_LANGUAGE ) {
90
- localizedHref = `/${ currentLanguage } ${ hrefString } `
145
+ localizedPathname = `/${ currentLanguage } ${ hrefString } `
91
146
}
92
147
148
+ const localizedHref = hrefObj
149
+ ? // copy the original UrlObject but replace/normalize pathname
150
+ ( { ...hrefObj , pathname : localizedPathname } as UrlObject )
151
+ : localizedPathname
152
+
93
153
return (
94
154
< Link
95
155
href = { localizedHref }
0 commit comments