@akinon/pz-theme 2.0.117-rc.0 → 2.0.118-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/CHANGELOG.md +15 -35
  2. package/package.json +4 -4
  3. package/src/blocks/__tests__/locale-links.test.tsx +429 -0
  4. package/src/blocks/account-button-block.tsx +1 -1
  5. package/src/blocks/account-menu-block.tsx +9 -3
  6. package/src/blocks/applied-filters-block.tsx +7 -1
  7. package/src/blocks/auth-form-block.tsx +7 -4
  8. package/src/blocks/basket-items-block.tsx +1 -1
  9. package/src/blocks/basket-summary-block.tsx +1 -1
  10. package/src/blocks/button-block.tsx +5 -1
  11. package/src/blocks/carousel-item-block.tsx +1 -1
  12. package/src/blocks/coupon-wallet-block.tsx +3 -1
  13. package/src/blocks/facet-filters-block.tsx +14 -10
  14. package/src/blocks/favorite-button-block.tsx +1 -1
  15. package/src/blocks/favourites-grid-block.tsx +1 -1
  16. package/src/blocks/group-block.tsx +19 -6
  17. package/src/blocks/hamburger-menu-block.tsx +1 -1
  18. package/src/blocks/hotspot-block.tsx +3 -1
  19. package/src/blocks/hover-showcase-item-block.tsx +3 -1
  20. package/src/blocks/link-block.tsx +5 -22
  21. package/src/blocks/listing-breadcrumb-block.tsx +1 -1
  22. package/src/blocks/listing-pagination-block.tsx +11 -3
  23. package/src/blocks/mega-menu-block.tsx +9 -3
  24. package/src/blocks/mini-basket-block.tsx +1 -1
  25. package/src/blocks/nav-menu-block.tsx +9 -3
  26. package/src/blocks/oauth-buttons-block.tsx +4 -0
  27. package/src/blocks/order-detail-block.tsx +1 -1
  28. package/src/blocks/order-list-block.tsx +1 -1
  29. package/src/blocks/order-success-block.tsx +1 -1
  30. package/src/blocks/otp-form-block.tsx +4 -1
  31. package/src/blocks/payment-badges-block.tsx +1 -1
  32. package/src/blocks/product-breadcrumb-block.tsx +1 -1
  33. package/src/blocks/product-grid-block.tsx +1 -1
  34. package/src/blocks/product-recommendations-block.tsx +1 -1
  35. package/src/blocks/register-form-block.tsx +6 -3
  36. package/src/blocks/search-box-block.tsx +7 -1
  37. package/src/blocks/social-icons-list-block.tsx +1 -1
  38. package/src/blocks/social-links-block.tsx +1 -1
  39. package/src/blocks/sort-select-block.tsx +8 -2
  40. package/src/blocks/store-locator-block.tsx +3 -1
  41. package/src/blocks/text-block.tsx +8 -1
  42. package/src/blocks/variant-selector-block.tsx +6 -2
  43. package/src/blocks/wishlist-button-block.tsx +1 -1
  44. package/src/components/__tests__/theme-link.test.tsx +169 -0
  45. package/src/components/theme-link.tsx +36 -0
  46. package/src/custom/README-sections.md +6 -0
  47. package/src/custom/README.md +71 -0
  48. package/src/custom/__tests__/custom-entry-exports.test.tsx +13 -0
  49. package/src/custom/__tests__/theme-editable.test.tsx +179 -0
  50. package/src/custom/__tests__/use-theme-setting.test.tsx +101 -0
  51. package/src/custom/index.ts +16 -0
  52. package/src/custom/theme-editable.tsx +57 -4
  53. package/src/custom/use-theme-setting.ts +77 -10
  54. package/src/hooks/use-localize-href.ts +34 -0
  55. package/src/index.ts +12 -0
  56. package/src/sections/find-in-store-section.tsx +3 -1
  57. package/src/sections/hover-showcase-section.tsx +6 -2
  58. package/src/sections/installment-options-section.tsx +3 -1
  59. package/src/sections/slider-quick-view.tsx +1 -1
  60. package/src/utils/__tests__/localize-href.test.ts +328 -0
  61. package/src/utils/localize-href.ts +192 -0
@@ -1,7 +1,7 @@
1
1
  'use client';
2
2
 
3
3
  import React from 'react';
4
- import NextLink from 'next/link';
4
+ import { ThemeLink as NextLink } from '../components/theme-link';
5
5
  import { getResponsiveValue, resolveOption } from '../utils';
6
6
  import { BlockRendererProps } from './block-renderer-registry';
7
7
  import { DesignerPlaceholder } from './product-block-utils';
@@ -8,6 +8,7 @@ import {
8
8
  } from '@akinon/next/data/client/account';
9
9
  import { useLocalization } from '@akinon/next/hooks';
10
10
  import { getResponsiveValue, partAttrs } from '../utils';
11
+ import { useLocalizeHref } from '../hooks/use-localize-href';
11
12
  import { formatDate, formatMoney } from './commerce-format';
12
13
  import { BlockRendererProps } from './block-renderer-registry';
13
14
  import { DesignerPlaceholder, readToggle } from './product-block-utils';
@@ -114,6 +115,7 @@ const CouponWalletBlock = ({
114
115
 
115
116
  const isMobile = currentBreakpoint === 'mobile';
116
117
  const { locale, currency } = useLocalization();
118
+ const localize = useLocalizeHref();
117
119
  const { status } = useSession();
118
120
  const isAuthenticated = status === 'authenticated';
119
121
 
@@ -159,7 +161,7 @@ const CouponWalletBlock = ({
159
161
  const renderTermsLink = () =>
160
162
  termsLinkLabel ? (
161
163
  <a
162
- href={isDesigner ? '#' : termsHref || '#'}
164
+ href={isDesigner ? '#' : localize(termsHref) || '#'}
163
165
  target={isDesigner ? undefined : termsLinkTarget}
164
166
  rel={termsLinkTarget === '_blank' ? 'noreferrer' : undefined}
165
167
  onClick={guard}
@@ -4,6 +4,7 @@ import React, { useEffect, useRef, useState } from 'react';
4
4
  import { usePathname, useRouter, useSearchParams } from 'next/navigation';
5
5
  import { useLocalization } from '@akinon/next/hooks';
6
6
  import { getResponsiveValue, partAttrs } from '../utils';
7
+ import { useLocalizeHref } from '../hooks/use-localize-href';
7
8
  import { useOverlay } from '../hooks/use-overlay';
8
9
  import { hasItemTemplate, ItemTemplateList } from './item-template';
9
10
  import { BlockRendererProps } from './block-renderer-registry';
@@ -309,6 +310,7 @@ const FacetFiltersBlock = ({
309
310
  const pathname = usePathname();
310
311
  const searchParams = useSearchParams();
311
312
  const { locale } = useLocalization();
313
+ const localize = useLocalizeHref();
312
314
  const [collapsedKeys, setCollapsedKeys] = useState<Record<string, boolean>>(
313
315
  {}
314
316
  );
@@ -786,6 +788,14 @@ const FacetFiltersBlock = ({
786
788
  return params;
787
789
  };
788
790
 
791
+ /**
792
+ * Every URL this block navigates to or renders. Commerce's `listing.path`
793
+ * has no locale; `localize` adds the page's (and leaves the one a
794
+ * `pathname` already carries).
795
+ */
796
+ const listingHref = (params: URLSearchParams): string =>
797
+ localize(buildListingHref(pathname || listing?.path || '', params));
798
+
789
799
  /**
790
800
  * The URL that toggles one choice. Split out from the click handler so a
791
801
  * designer-authored option row can bind a link to `item.url` and still
@@ -795,8 +805,7 @@ const FacetFiltersBlock = ({
795
805
  facet: ListingFacet,
796
806
  choice: ListingFacetChoice
797
807
  ): string =>
798
- buildListingHref(
799
- pathname || listing?.path || '',
808
+ listingHref(
800
809
  toggleChoiceParams(
801
810
  new URLSearchParams(appliedQuery),
802
811
  facet,
@@ -810,7 +819,7 @@ const FacetFiltersBlock = ({
810
819
  const params = deletePageCursor(new URLSearchParams(appliedQuery));
811
820
  params.delete(facet.searchKey);
812
821
 
813
- return buildListingHref(pathname || listing?.path || '', params);
822
+ return listingHref(params);
814
823
  };
815
824
 
816
825
  const handleSelect = (facet: ListingFacet, choice: ListingFacetChoice) => {
@@ -1407,7 +1416,7 @@ const FacetFiltersBlock = ({
1407
1416
  const params = deletePageCursor(new URLSearchParams(appliedQuery));
1408
1417
  facets.forEach((facet) => params.delete(facet.searchKey));
1409
1418
 
1410
- router.push(buildListingHref(pathname || listing?.path || '', params));
1419
+ router.push(listingHref(params));
1411
1420
  };
1412
1421
 
1413
1422
  const hasStagedChanges =
@@ -1427,12 +1436,7 @@ const FacetFiltersBlock = ({
1427
1436
  */
1428
1437
  const handleApply = () => {
1429
1438
  if (!isDesigner && hasStagedChanges && stagedQuery !== null) {
1430
- router.push(
1431
- buildListingHref(
1432
- pathname || listing?.path || '',
1433
- new URLSearchParams(stagedQuery)
1434
- )
1435
- );
1439
+ router.push(listingHref(new URLSearchParams(stagedQuery)));
1436
1440
  }
1437
1441
 
1438
1442
  setPanelOpen(false);
@@ -1,7 +1,7 @@
1
1
  'use client';
2
2
 
3
3
  import React from 'react';
4
- import NextLink from 'next/link';
4
+ import { ThemeLink as NextLink } from '../components/theme-link';
5
5
  import { useSession } from 'next-auth/react';
6
6
  import {
7
7
  useAddFavoriteMutation,
@@ -1,7 +1,7 @@
1
1
  'use client';
2
2
 
3
3
  import React from 'react';
4
- import NextLink from 'next/link';
4
+ import { ThemeLink as NextLink } from '../components/theme-link';
5
5
  import { useSession } from 'next-auth/react';
6
6
  import {
7
7
  useGetFavoritesQuery,
@@ -1,6 +1,7 @@
1
1
  import React from 'react';
2
- import NextLink from 'next/link';
2
+ import { ThemeLink as NextLink } from '../components/theme-link';
3
3
  import { getResponsiveValue } from '../utils';
4
+ import { asSitePath, isRootRelativeHref } from '../utils/localize-href';
4
5
  import {
5
6
  buildCommerceItem,
6
7
  emitCommerceEvent,
@@ -17,9 +18,12 @@ import { renderComposedChildren } from './render-composed-children';
17
18
  * composition data (wishlist context, carousel items) does not reliably
18
19
  * survive. Router navigation keeps the app state alive in both directions.
19
20
  * External hrefs keep the plain anchor — the router cannot take those anyway.
21
+ *
22
+ * The package's own test (`isRootRelativeHref`) rather than a local one: it
23
+ * trims, and a pasted ' /yatak-odasi/' used to miss this branch and go out as
24
+ * a plain anchor — which also meant it never reached `ThemeLink` and lost the
25
+ * page's locale with it.
20
26
  */
21
- const isInternalHref = (href: unknown): href is string =>
22
- typeof href === 'string' && href.startsWith('/') && !href.startsWith('//');
23
27
  const hasOwnLength = (value: unknown, breakpoint: string): boolean => {
24
28
  const raw = String(getResponsiveValue(value, breakpoint, '')).trim();
25
29
  return raw !== '' && raw !== 'auto' && raw !== '0' && raw !== '0px';
@@ -65,7 +69,16 @@ const GroupBlock = ({
65
69
  const tagProps: Record<string, unknown> = {
66
70
  className: 'contents'
67
71
  };
68
- const useRouterLink = tag === 'a' && !isDesigner && isInternalHref(href);
72
+ // Trimmed once here, because it is what the router link is given. A full URL
73
+ // of the storefront's own counts as one of its routes as well: a merchant
74
+ // pastes the address bar into a group as readily as into a link, and
75
+ // `link-block` already routes that form through `ThemeLink`.
76
+ const routerHref = isRootRelativeHref(href)
77
+ ? href.trim()
78
+ : typeof href === 'string'
79
+ ? asSitePath(href.trim(), process.env.NEXT_PUBLIC_URL)
80
+ : undefined;
81
+ const useRouterLink = tag === 'a' && !isDesigner && routerHref !== undefined;
69
82
  if (tag === 'a' && href && !useRouterLink) {
70
83
  tagProps.href = href;
71
84
  }
@@ -105,7 +118,7 @@ const GroupBlock = ({
105
118
  if (useRouterLink) {
106
119
  return (
107
120
  <NextLink
108
- href={href as string}
121
+ href={routerHref as string}
109
122
  prefetch={false}
110
123
  className="contents"
111
124
  onClick={handleAnchorAnalytics}
@@ -154,7 +167,7 @@ const GroupBlock = ({
154
167
  if (useRouterLink) {
155
168
  return (
156
169
  <NextLink
157
- href={href as string}
170
+ href={routerHref as string}
158
171
  prefetch={false}
159
172
  className="contents"
160
173
  onClick={handleAnchorAnalytics}
@@ -1,7 +1,7 @@
1
1
  'use client';
2
2
 
3
3
  import React, { useState } from 'react';
4
- import NextLink from 'next/link';
4
+ import { ThemeLink as NextLink } from '../components/theme-link';
5
5
  import { useGetMenuQuery } from '@akinon/next/data/client/misc';
6
6
  import { getResponsiveValue, partAttrs, resolveForceOpen } from '../utils';
7
7
  import { useOverlay } from '../hooks/use-overlay';
@@ -5,6 +5,7 @@ import { BlockRendererProps } from './block-renderer-registry';
5
5
  import { getResponsiveValue, getCSSStyles, resolveForceOpen } from '../utils';
6
6
  import { useThemeSettingsContext } from '../theme-settings-context';
7
7
  import { useLocalization } from '@akinon/next/hooks';
8
+ import { useLocalizeHref } from '../hooks/use-localize-href';
8
9
  import ThemeBlock from '../theme-block';
9
10
 
10
11
  import { postToEditor } from '../editor-bridge';
@@ -19,6 +20,7 @@ const HotspotBlock = ({
19
20
  const [isOpen, setIsOpen] = useState(false);
20
21
  const themeSettings = useThemeSettingsContext();
21
22
  const { locale } = useLocalization();
23
+ const localize = useLocalizeHref();
22
24
  const defaultLocale = process.env.NEXT_PUBLIC_DEFAULT_LOCALE || 'en';
23
25
 
24
26
  const styles = block.styles || {};
@@ -224,7 +226,7 @@ const HotspotBlock = ({
224
226
 
225
227
  {link && (
226
228
  <a
227
- href={link}
229
+ href={localize(link)}
228
230
  className="text-blue-600 hover:underline font-medium block mt-2"
229
231
  onClick={(e) => e.stopPropagation()}
230
232
  >
@@ -2,6 +2,7 @@
2
2
 
3
3
  import React from 'react';
4
4
  import { useLocalization } from '@akinon/next/hooks';
5
+ import { useLocalizeHref } from '../hooks/use-localize-href';
5
6
  import { getResponsiveValue, partAttrs } from '../utils';
6
7
  import { BlockRendererProps } from './block-renderer-registry';
7
8
  import { DesignerPlaceholder } from './product-block-utils';
@@ -73,6 +74,7 @@ const HoverShowcaseItemBlock = ({
73
74
  isDesigner = false
74
75
  }: BlockRendererProps) => {
75
76
  const { locale } = useLocalization();
77
+ const localize = useLocalizeHref();
76
78
  const defaultLocale = process.env.NEXT_PUBLIC_DEFAULT_LOCALE || 'en';
77
79
 
78
80
  /**
@@ -200,7 +202,7 @@ const HoverShowcaseItemBlock = ({
200
202
  // is nothing to hang the anchor on, so an authored link is simply not
201
203
  // reachable — the same as in the section today.
202
204
  <a
203
- href={link}
205
+ href={localize(link)}
204
206
  data-hover-showcase-link=""
205
207
  {...partAttrs('link')}
206
208
  onClick={handleLinkClick}
@@ -1,10 +1,8 @@
1
1
  'use client';
2
2
 
3
3
  import React, { useMemo } from 'react';
4
- import NextLink from 'next/link';
5
- import { useLocalization } from '@akinon/next/hooks';
6
- import { LocaleUrlStrategy } from '@akinon/next/localization';
7
- import { urlLocaleMatcherRegex, urlSchemes } from '@akinon/next/utils';
4
+ import { ThemeLink as NextLink } from '../components/theme-link';
5
+ import { urlSchemes } from '@akinon/next/utils';
8
6
  import {
9
7
  getInnerRootInlineStyles,
10
8
  getResponsiveValue,
@@ -25,7 +23,6 @@ const LinkBlock = ({
25
23
  currentBreakpoint = 'desktop',
26
24
  isDesigner = false
27
25
  }: BlockRendererProps) => {
28
- const { locale, defaultLocaleValue, localeUrlStrategy } = useLocalization();
29
26
  const themeSettings = useThemeSettingsContext();
30
27
  const action = useBlockAction(block, { currentBreakpoint, isDesigner });
31
28
 
@@ -93,23 +90,9 @@ const LinkBlock = ({
93
90
  return '#';
94
91
  }
95
92
 
96
- const pathnameWithoutLocale = trimmedHref.replace(
97
- urlLocaleMatcherRegex,
98
- ''
99
- );
100
- const hrefWithLocale = `/${locale}${pathnameWithoutLocale}`;
101
-
102
- if (localeUrlStrategy === LocaleUrlStrategy.ShowAllLocales) {
103
- return hrefWithLocale;
104
- } else if (
105
- localeUrlStrategy === LocaleUrlStrategy.HideDefaultLocale &&
106
- locale !== defaultLocaleValue
107
- ) {
108
- return hrefWithLocale;
109
- }
110
-
111
- return trimmedHref || '#';
112
- }, [href, defaultLocaleValue, locale, localeUrlStrategy]);
93
+ // The locale is ThemeLink's job, the same as for every other block link.
94
+ return trimmedHref;
95
+ }, [href]);
113
96
 
114
97
  /**
115
98
  * Only the values the generated stylesheet cannot produce; the rest now
@@ -1,7 +1,7 @@
1
1
  'use client';
2
2
 
3
3
  import React from 'react';
4
- import NextLink from 'next/link';
4
+ import { ThemeLink as NextLink } from '../components/theme-link';
5
5
  import { getResponsiveValue, partAttrs } from '../utils';
6
6
  import { BlockRendererProps } from './block-renderer-registry';
7
7
  import {
@@ -1,10 +1,11 @@
1
1
  'use client';
2
2
 
3
3
  import React from 'react';
4
- import NextLink from 'next/link';
4
+ import { ThemeLink as NextLink } from '../components/theme-link';
5
5
  import { usePathname, useSearchParams } from 'next/navigation';
6
6
  import { useLocalization } from '@akinon/next/hooks';
7
7
  import { getResponsiveValue, partAttrs, resolveOption } from '../utils';
8
+ import { useLocalizeHref } from '../hooks/use-localize-href';
8
9
  import { hasItemTemplate, ItemTemplateList } from './item-template';
9
10
  import { BlockRendererProps } from './block-renderer-registry';
10
11
  import {
@@ -301,6 +302,7 @@ const ListingPaginationBlock = ({
301
302
  const pathname = usePathname();
302
303
  const searchParams = useSearchParams();
303
304
  const { locale } = useLocalization();
305
+ const localize = useLocalizeHref();
304
306
 
305
307
  const mode = String(
306
308
  getResponsiveValue(block.properties?.mode, currentBreakpoint, 'pages')
@@ -455,9 +457,15 @@ const ListingPaginationBlock = ({
455
457
  params.set(queryParam, String(page));
456
458
  }
457
459
 
458
- return buildListingHref(pathname || listing?.path || '', params);
460
+ // Linked, fetched and written to history alike, so it carries the page's
461
+ // locale: an unprefixed fetch would come back as the default-locale page
462
+ // (and switch the locale cookie with it). Commerce's `listing.path` has
463
+ // none; one a `pathname` already carries is kept.
464
+ return localize(
465
+ buildListingHref(pathname || listing?.path || '', params)
466
+ );
459
467
  },
460
- [searchParams, queryParam, pathname, listing?.path]
468
+ [searchParams, queryParam, pathname, listing?.path, localize]
461
469
  );
462
470
 
463
471
  const loadNextPage = React.useCallback(async () => {
@@ -7,8 +7,9 @@ import React, {
7
7
  useRef,
8
8
  useState
9
9
  } from 'react';
10
- import NextLink from 'next/link';
10
+ import { ThemeLink as NextLink } from '../components/theme-link';
11
11
  import { usePathname } from 'next/navigation';
12
+ import { useLocalizeHref } from '../hooks/use-localize-href';
12
13
  import { useGetMenuQuery } from '@akinon/next/data/client/misc';
13
14
  import { getResponsiveValue, partAttrs, resolveForceOpen } from '../utils';
14
15
  import { useOverlay } from '../hooks/use-overlay';
@@ -206,6 +207,7 @@ const MegaMenuBlock = ({
206
207
  isDesigner = false
207
208
  }: BlockRendererProps) => {
208
209
  const pathname = usePathname();
210
+ const localize = useLocalizeHref();
209
211
  const [openKey, setOpenKey] = useState<string | null>(null);
210
212
  const openTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
211
213
  const closeTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
@@ -402,12 +404,16 @@ const MegaMenuBlock = ({
402
404
 
403
405
  // The entry for the page the shopper is on carries `selected` so a part
404
406
  // style can address it without the brand hand-listing every label.
407
+ //
408
+ // Menu URLs are stored without a locale while the pathname carries it
409
+ // (`/en/yatak-odasi`), so the entry is matched in the localized form its link
410
+ // renders — and so is the home entry the section match must never widen to.
405
411
  const isCurrent = (url: string) => {
406
- const target = normalizePath(url);
412
+ const target = normalizePath(localize(url));
407
413
  const current = normalizePath(pathname);
408
414
 
409
415
  if (!target || target === '#' || !current) return false;
410
- if (activeMatch === 'section' && target !== '/') {
416
+ if (activeMatch === 'section' && target !== normalizePath(localize('/'))) {
411
417
  return current === target || current.startsWith(`${target}/`);
412
418
  }
413
419
 
@@ -1,7 +1,7 @@
1
1
  'use client';
2
2
 
3
3
  import React from 'react';
4
- import NextLink from 'next/link';
4
+ import { ThemeLink as NextLink } from '../components/theme-link';
5
5
  import { useGetBasketQuery } from '@akinon/next/data/client/basket';
6
6
  import { getResponsiveValue, partAttrs } from '../utils';
7
7
  import { BlockRendererProps } from './block-renderer-registry';
@@ -1,8 +1,9 @@
1
1
  'use client';
2
2
 
3
3
  import React, { useState } from 'react';
4
- import NextLink from 'next/link';
4
+ import { ThemeLink as NextLink } from '../components/theme-link';
5
5
  import { usePathname } from 'next/navigation';
6
+ import { useLocalizeHref } from '../hooks/use-localize-href';
6
7
  import { useGetMenuQuery } from '@akinon/next/data/client/misc';
7
8
  import { getResponsiveValue, partAttrs, resolveForceOpen } from '../utils';
8
9
  import { useMenuSeed } from '../menu-seed-context';
@@ -53,6 +54,7 @@ const NavMenuBlock = ({
53
54
  isDesigner = false
54
55
  }: BlockRendererProps) => {
55
56
  const pathname = usePathname();
57
+ const localize = useLocalizeHref();
56
58
  const [openKey, setOpenKey] = useState<string | null>(null);
57
59
 
58
60
  // Shared with the server's menu collector, so the key it seeded under is the
@@ -130,12 +132,16 @@ const NavMenuBlock = ({
130
132
 
131
133
  // The entry for the page the shopper is on is what a brand reaches for first
132
134
  // after colour; it carries `selected` so a part style can address it.
135
+ //
136
+ // Menu URLs are stored without a locale while the pathname carries it
137
+ // (`/en/yatak-odasi`), so the entry is matched in the localized form its link
138
+ // renders — and so is the home entry the section match must never widen to.
133
139
  const isCurrent = (url?: string) => {
134
- const target = normalizePath(url);
140
+ const target = normalizePath(localize(url ?? ''));
135
141
  const current = normalizePath(pathname);
136
142
 
137
143
  if (!target || target === '#' || !current) return false;
138
- if (activeMatch === 'section' && target !== '/') {
144
+ if (activeMatch === 'section' && target !== normalizePath(localize('/'))) {
139
145
  return current === target || current.startsWith(`${target}/`);
140
146
  }
141
147
 
@@ -1,6 +1,10 @@
1
1
  'use client';
2
2
 
3
3
  import React from 'react';
4
+ // Not ThemeLink: `withOauthLogin` only matches an unprefixed
5
+ // `/{provider}/login/`, so there is no locale to add. `localizeHref` leaves that
6
+ // route alone too (`OAUTH_ROUTE`), which keeps a row template that renders
7
+ // `item.url` through a button or group working on an English page.
4
8
  import NextLink from 'next/link';
5
9
  import { getResponsiveValue, partAttrs } from '../utils';
6
10
  import { DesignerPlaceholder, readToggle } from './product-block-utils';
@@ -1,7 +1,7 @@
1
1
  'use client';
2
2
 
3
3
  import React from 'react';
4
- import NextLink from 'next/link';
4
+ import { ThemeLink as NextLink } from '../components/theme-link';
5
5
  import { useParams, useSearchParams } from 'next/navigation';
6
6
  import { useGetOrderQuery } from '@akinon/next/data/client/account';
7
7
  import { useLocalization } from '@akinon/next/hooks';
@@ -1,7 +1,7 @@
1
1
  'use client';
2
2
 
3
3
  import React, { useEffect, useState } from 'react';
4
- import NextLink from 'next/link';
4
+ import { ThemeLink as NextLink } from '../components/theme-link';
5
5
  import { useSession } from 'next-auth/react';
6
6
  import { useGetOrdersQuery } from '@akinon/next/data/client/account';
7
7
  import { useLocalization } from '@akinon/next/hooks';
@@ -1,7 +1,7 @@
1
1
  'use client';
2
2
 
3
3
  import React from 'react';
4
- import NextLink from 'next/link';
4
+ import { ThemeLink as NextLink } from '../components/theme-link';
5
5
  import { usePathname, useSearchParams } from 'next/navigation';
6
6
  import { useFetchCheckoutResultQuery } from '@akinon/next/data/client/checkout';
7
7
  import { useLocalization } from '@akinon/next/hooks';
@@ -6,6 +6,7 @@ import { signIn } from 'next-auth/react';
6
6
  import { useLocalization } from '@akinon/next/hooks';
7
7
  import { useOtpLoginMutation } from '@akinon/next/data/client/user';
8
8
  import { getResponsiveValue, partAttrs, resolveOption } from '../utils';
9
+ import { useLocalizeHref } from '../hooks/use-localize-href';
9
10
  import { readToggle } from './product-block-utils';
10
11
  import { closeAllModals } from '../overlays/overlay-bus';
11
12
  import {
@@ -273,6 +274,7 @@ const OtpFormBlock = ({
273
274
  );
274
275
 
275
276
  const router = useRouter();
277
+ const localize = useLocalizeHref();
276
278
  const search = useSearchParams();
277
279
  const pathname = usePathname();
278
280
  const destination = resolveAuthDestination({
@@ -565,7 +567,8 @@ const OtpFormBlock = ({
565
567
  router.refresh();
566
568
  // Our own resolved path rather than next-auth's echo of it; `null` is
567
569
  // `stay`, where the refresh above is the whole navigation.
568
- if (destination) router.push(destination);
570
+ // An authored destination has no locale; a returned one keeps its own.
571
+ if (destination) router.push(localize(destination));
569
572
  } catch {
570
573
  setError(errorLabel);
571
574
  } finally {
@@ -1,7 +1,7 @@
1
1
  'use client';
2
2
 
3
3
  import React, { useMemo } from 'react';
4
- import NextLink from 'next/link';
4
+ import { ThemeLink as NextLink } from '../components/theme-link';
5
5
  import ThemeBlock, { Block } from '../theme-block';
6
6
  import { getResponsiveValue, partAttrs } from '../utils';
7
7
  import { BlockRendererProps } from './block-renderer-registry';
@@ -1,7 +1,7 @@
1
1
  'use client';
2
2
 
3
3
  import React from 'react';
4
- import NextLink from 'next/link';
4
+ import { ThemeLink as NextLink } from '../components/theme-link';
5
5
  import { getResponsiveValue, partAttrs } from '../utils';
6
6
  import { useThemePageContext } from '../theme-page-context';
7
7
  import { BlockRendererProps } from './block-renderer-registry';
@@ -1,7 +1,7 @@
1
1
  'use client';
2
2
 
3
3
  import React from 'react';
4
- import NextLink from 'next/link';
4
+ import { ThemeLink as NextLink } from '../components/theme-link';
5
5
  import { getResponsiveValue, partAttrs } from '../utils';
6
6
  import {
7
7
  buildCommerceItem,
@@ -1,7 +1,7 @@
1
1
  'use client';
2
2
 
3
3
  import React, { useRef } from 'react';
4
- import NextLink from 'next/link';
4
+ import { ThemeLink as NextLink } from '../components/theme-link';
5
5
  import { useSession } from 'next-auth/react';
6
6
  import { useGetWidgetQuery } from '@akinon/next/data/client/misc';
7
7
  import { useGetFavoritesQuery } from '@akinon/next/data/client/wishlist';
@@ -5,6 +5,7 @@ import { usePathname, useRouter, useSearchParams } from 'next/navigation';
5
5
  import { signIn, useSession } from 'next-auth/react';
6
6
  import { useLocalization } from '@akinon/next/hooks';
7
7
  import { getResponsiveValue, partAttrs, resolveOption } from '../utils';
8
+ import { useLocalizeHref } from '../hooks/use-localize-href';
8
9
  import { readToggle } from './product-block-utils';
9
10
  import { closeAllModals } from '../overlays/overlay-bus';
10
11
  import {
@@ -293,6 +294,7 @@ const RegisterFormBlock = ({
293
294
  );
294
295
 
295
296
  const router = useRouter();
297
+ const localize = useLocalizeHref();
296
298
  const search = useSearchParams();
297
299
  const pathname = usePathname();
298
300
  const { status } = useSession();
@@ -351,8 +353,8 @@ const RegisterFormBlock = ({
351
353
  React.useEffect(() => {
352
354
  if (!isSignedIn || whenSignedIn !== 'redirect') return;
353
355
 
354
- router.replace(resolveSignedInDestination(signedInHref, search));
355
- }, [isSignedIn, whenSignedIn, signedInHref, router, search]);
356
+ router.replace(localize(resolveSignedInDestination(signedInHref, search)));
357
+ }, [isSignedIn, whenSignedIn, signedInHref, router, search, localize]);
356
358
 
357
359
  /**
358
360
  * The form's contents, wrapped so a Security Check block (or the form's own
@@ -718,7 +720,8 @@ const RegisterFormBlock = ({
718
720
 
719
721
  // Our own resolved path rather than next-auth's echo of it: this one has
720
722
  // been through the same-origin check. `null` is `stay`.
721
- if (destination) router.push(destination);
723
+ // An authored destination has no locale; a returned one keeps its own.
724
+ if (destination) router.push(localize(destination));
722
725
  } catch {
723
726
  setError(errorLabel);
724
727
  } finally {
@@ -4,6 +4,7 @@ import React, { useRef, useState } from 'react';
4
4
  import { useRouter } from 'next/navigation';
5
5
  import { getResponsiveValue, partAttrs, resolveForceOpen } from '../utils';
6
6
  import { emitCommerceEvent } from '../utils/analytics';
7
+ import { useLocalizeHref } from '../hooks/use-localize-href';
7
8
  import { applySearchTermTransform } from '../utils/search-transform';
8
9
  import { useOverlay } from '../hooks/use-overlay';
9
10
  import ThemeBlock from '../theme-block';
@@ -49,6 +50,7 @@ const SearchBoxBlock = ({
49
50
  isDesigner = false
50
51
  }: BlockRendererProps) => {
51
52
  const router = useRouter();
53
+ const localize = useLocalizeHref();
52
54
  const [open, setOpen] = useState(false);
53
55
  const [term, setTerm] = useState('');
54
56
  const triggerRef = useRef<HTMLButtonElement | null>(null);
@@ -190,8 +192,12 @@ const SearchBoxBlock = ({
190
192
  if (!value || isDesigner) return;
191
193
  emitCommerceEvent('search', { searchTerm: value });
192
194
  const separator = searchUrl.includes('?') ? '&' : '?';
195
+ // The authored `/list` has no locale; a search from an English page must
196
+ // land on the English results.
193
197
  router.push(
194
- `${searchUrl}${separator}${queryParam}=${encodeURIComponent(value)}`
198
+ localize(
199
+ `${searchUrl}${separator}${queryParam}=${encodeURIComponent(value)}`
200
+ )
195
201
  );
196
202
  if (clearOnSubmit) setTerm('');
197
203
  setOpen(false);
@@ -1,7 +1,7 @@
1
1
  'use client';
2
2
 
3
3
  import React from 'react';
4
- import NextLink from 'next/link';
4
+ import { ThemeLink as NextLink } from '../components/theme-link';
5
5
  import { partAttrs } from '../utils';
6
6
  import { BlockRendererProps } from './block-renderer-registry';
7
7
  import { DesignerPlaceholder } from './product-block-utils';
@@ -1,7 +1,7 @@
1
1
  'use client';
2
2
 
3
3
  import React from 'react';
4
- import NextLink from 'next/link';
4
+ import { ThemeLink as NextLink } from '../components/theme-link';
5
5
  import { getResponsiveValue, partAttrs } from '../utils';
6
6
  import { BlockRendererProps } from './block-renderer-registry';
7
7
  import { DesignerPlaceholder, readToggle } from './product-block-utils';