@usequeek/theme-kit 0.1.20 → 0.1.22

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 (94) hide show
  1. package/README.md +264 -77
  2. package/api/collections.ts +5 -5
  3. package/api/shop.ts +1 -1
  4. package/client.ts +2 -0
  5. package/components/auth/auth-email-login-step.tsx +9 -7
  6. package/components/auth/auth-email-otp-step.tsx +9 -7
  7. package/components/auth/auth-email-otp-verify-step.tsx +8 -6
  8. package/components/auth/auth-email-register-step.tsx +10 -8
  9. package/components/auth/auth-signup-step.tsx +8 -6
  10. package/components/blog/category-filter.tsx +6 -5
  11. package/components/blog/pagination.tsx +8 -7
  12. package/components/blog/post-meta.tsx +15 -8
  13. package/components/blog/related-posts.tsx +5 -3
  14. package/components/blog/share-buttons.tsx +10 -8
  15. package/components/checkout/checkout-contact.tsx +9 -7
  16. package/components/checkout/default-checkout-shell.tsx +17 -15
  17. package/components/checkout/policy-links.tsx +5 -3
  18. package/components/image.tsx +5 -5
  19. package/components/language-switcher.css +131 -0
  20. package/components/language-switcher.tsx +144 -0
  21. package/components/markdown.tsx +1 -1
  22. package/components/powered-by-queek.tsx +4 -4
  23. package/components/product-media-frame.tsx +1 -1
  24. package/components/product-metafields.tsx +2 -3
  25. package/components/variant-picker.tsx +6 -9
  26. package/demo.ts +10 -14
  27. package/design-token-preview.tsx +0 -2
  28. package/edit-mode-active.tsx +3 -4
  29. package/hooks/use-announcement.ts +1 -1
  30. package/hooks/use-auth-flow.tsx +19 -18
  31. package/hooks/use-locales.ts +107 -0
  32. package/hooks/use-posts.ts +1 -1
  33. package/hooks/use-product-detail.ts +1 -2
  34. package/hooks/use-product-sections.ts +4 -6
  35. package/hooks/use-products.ts +5 -8
  36. package/index.ts +16 -15
  37. package/locales/en.default.json +49 -7
  38. package/next-reference.d.ts +2 -4
  39. package/package.json +3 -2
  40. package/page-renderer.tsx +5 -6
  41. package/provider.tsx +22 -2
  42. package/scripts/verify-package.ts +6 -8
  43. package/sdk/client.ts +13 -12
  44. package/sdk/error-handler.ts +1 -1
  45. package/sdk/platform.ts +6 -8
  46. package/sdk/queek-client.ts +11 -12
  47. package/sdk/server-store-client.ts +13 -17
  48. package/shared-blocks/metaobjects.tsx +1 -1
  49. package/stores/cart-store.ts +2 -2
  50. package/stores/checkout-store.ts +1 -1
  51. package/stores/edit-preview-store.ts +5 -6
  52. package/stores/user-store.ts +4 -5
  53. package/strings/pseudo-locale.ts +263 -0
  54. package/strings/theme-strings.ts +185 -20
  55. package/tests/auth-email-errors.test.tsx +55 -0
  56. package/tests/currency-contract.test.ts +2 -3
  57. package/tests/display-format.test.ts +356 -2
  58. package/tests/fixtures/external-theme.tsx +18 -2
  59. package/tests/language-switcher.test.tsx +350 -0
  60. package/tests/list-pagination.test.tsx +8 -8
  61. package/tests/locale-switch.test.ts +197 -0
  62. package/tests/locale-transport.test.tsx +3 -3
  63. package/tests/markdown-render.test.ts +5 -6
  64. package/tests/orders-walk.test.ts +5 -5
  65. package/tests/page-renderer-signature.test.tsx +1 -1
  66. package/tests/powered-by-queek.test.tsx +3 -3
  67. package/tests/powered-by-url.test.ts +2 -2
  68. package/tests/public-surface.test.ts +1 -4
  69. package/tests/shopper-chrome-strings.test.tsx +271 -0
  70. package/tests/storefront-client-scope.test.ts +5 -5
  71. package/tests/theme-hook-placement.test.tsx +179 -0
  72. package/tests/theme-strings-client.test.tsx +5 -5
  73. package/tests/theme-strings-pseudo.test.tsx +171 -0
  74. package/tests/theme-strings-typed.test.tsx +97 -0
  75. package/tests/theme-strings.test.tsx +10 -7
  76. package/tests/theme-typed-keys.fixture.ts +76 -0
  77. package/tests/use-shop-prefetch.test.tsx +1 -1
  78. package/theme-mount.tsx +6 -11
  79. package/types/block.ts +7 -7
  80. package/types/media.ts +8 -9
  81. package/types/product.ts +8 -12
  82. package/types/theme.ts +26 -25
  83. package/types/vendor.ts +10 -12
  84. package/utils/announcement.ts +3 -3
  85. package/utils/benefit-icon.tsx +9 -11
  86. package/utils/brand.ts +7 -10
  87. package/utils/format.ts +100 -14
  88. package/utils/image-srcset.ts +2 -2
  89. package/utils/locale-switch.ts +208 -0
  90. package/utils/locale.ts +56 -5
  91. package/utils/menu-link.ts +1 -1
  92. package/utils/product-media.ts +1 -1
  93. package/utils/product-normalizer.ts +2 -2
  94. package/utils/vendor-host.ts +7 -11
@@ -1,18 +1,20 @@
1
1
  'use client';
2
2
  import type { JSX } from 'react';
3
3
  import { useAuthFlow } from '../../hooks/use-auth-flow';
4
+ import { useThemeStrings } from '../../provider';
4
5
 
5
6
  export function AuthEmailRegisterStep(): JSX.Element {
6
7
  const { firstName, setFirstName, lastName, setLastName, email, setEmail, password, setPassword, submitEmailRegister, loading, error, goToEmailLogin } = useAuthFlow();
8
+ const t = useThemeStrings();
7
9
 
8
10
  return (
9
11
  <div className="core-auth__step core-auth__step--email-register">
10
- <h2 className="core-auth__title">Create account</h2>
11
- <p className="core-auth__sub">Fill in your details to get started</p>
12
+ <h2 className="core-auth__title">{t('auth.register.title')}</h2>
13
+ <p className="core-auth__sub">{t('auth.register.subtitle')}</p>
12
14
  <input
13
15
  type="text"
14
16
  className="core-input"
15
- placeholder="First name"
17
+ placeholder={t('auth.register.first')}
16
18
  value={firstName}
17
19
  onChange={(e) => setFirstName(e.target.value)}
18
20
  autoComplete="given-name"
@@ -21,7 +23,7 @@ export function AuthEmailRegisterStep(): JSX.Element {
21
23
  <input
22
24
  type="text"
23
25
  className="core-input"
24
- placeholder="Last name (optional)"
26
+ placeholder={t('auth.register.last')}
25
27
  value={lastName}
26
28
  onChange={(e) => setLastName(e.target.value)}
27
29
  autoComplete="family-name"
@@ -29,7 +31,7 @@ export function AuthEmailRegisterStep(): JSX.Element {
29
31
  <input
30
32
  type="email"
31
33
  className="core-input"
32
- placeholder="Email address"
34
+ placeholder={t('auth.register.email')}
33
35
  value={email}
34
36
  onChange={(e) => setEmail(e.target.value)}
35
37
  autoComplete="email"
@@ -37,7 +39,7 @@ export function AuthEmailRegisterStep(): JSX.Element {
37
39
  <input
38
40
  type="password"
39
41
  className="core-input"
40
- placeholder="Password (min 8 characters)"
42
+ placeholder={t('auth.register.password')}
41
43
  value={password}
42
44
  onChange={(e) => setPassword(e.target.value)}
43
45
  onKeyDown={(e) => { if (e.key === 'Enter') void submitEmailRegister(); }}
@@ -50,10 +52,10 @@ export function AuthEmailRegisterStep(): JSX.Element {
50
52
  onClick={() => void submitEmailRegister()}
51
53
  disabled={loading || !firstName.trim() || !email.trim() || password.length < 8}
52
54
  >
53
- {loading ? 'Creating account…' : 'Create account'}
55
+ {loading ? t('auth.register.creating') : t('auth.register.title')}
54
56
  </button>
55
57
  <button type="button" className="core-auth__link" onClick={goToEmailLogin}>
56
- Already have an account? Sign in
58
+ {t('auth.register.signin')}
57
59
  </button>
58
60
  </div>
59
61
  );
@@ -2,6 +2,7 @@
2
2
 
3
3
  import type { JSX } from 'react';
4
4
  import { useAuthFlow } from '../../hooks/use-auth-flow';
5
+ import { useThemeStrings } from '../../provider';
5
6
 
6
7
  export function AuthSignupStep(): JSX.Element {
7
8
  const {
@@ -11,16 +12,17 @@ export function AuthSignupStep(): JSX.Element {
11
12
  loading, error,
12
13
  goToEmailOtp,
13
14
  } = useAuthFlow();
15
+ const t = useThemeStrings();
14
16
 
15
17
  return (
16
18
  <div className="core-auth__step core-auth__step--signup">
17
- <h2 className="core-auth__title">Create your account</h2>
18
- <p className="core-auth__sub">Just a few details to get you started</p>
19
+ <h2 className="core-auth__title">{t('auth.signup.title')}</h2>
20
+ <p className="core-auth__sub">{t('auth.signup.subtitle')}</p>
19
21
 
20
22
  <input
21
23
  type="text"
22
24
  className="core-input"
23
- placeholder="First name"
25
+ placeholder={t('auth.signup.first')}
24
26
  value={firstName}
25
27
  onChange={(e) => setFirstName(e.target.value)}
26
28
  autoComplete="given-name"
@@ -29,7 +31,7 @@ export function AuthSignupStep(): JSX.Element {
29
31
  <input
30
32
  type="text"
31
33
  className="core-input"
32
- placeholder="Last name (optional)"
34
+ placeholder={t('auth.signup.last')}
33
35
  value={lastName}
34
36
  onChange={(e) => setLastName(e.target.value)}
35
37
  autoComplete="family-name"
@@ -43,11 +45,11 @@ export function AuthSignupStep(): JSX.Element {
43
45
  onKeyDown={(e) => { if (e.key === 'Enter') void submitRegister(); }}
44
46
  disabled={loading || !firstName.trim()}
45
47
  >
46
- {loading ? 'Creating account…' : 'Create account'}
48
+ {loading ? t('auth.signup.creating') : t('auth.signup.create')}
47
49
  </button>
48
50
 
49
51
  <button type="button" className="core-auth__link" onClick={goToEmailOtp}>
50
- ← Use a different email
52
+ {t('auth.signup.different')}
51
53
  </button>
52
54
  </div>
53
55
  );
@@ -1,9 +1,9 @@
1
1
  'use client';
2
2
 
3
+ import { useStorefront, useThemeStrings } from '../../provider';
3
4
  import type { JSX } from 'react';
4
5
  import Link from 'next/link';
5
6
  import type { BlogCategory } from '../../types/page';
6
- import { useStorefront } from '../../provider';
7
7
 
8
8
  /**
9
9
  * Shared blog category browse bar. Links to `${basePath}/blog?category=slug`.
@@ -28,6 +28,7 @@ export function CategoryFilter({
28
28
  className?: string;
29
29
  }): JSX.Element | null {
30
30
  const { basePath } = useStorefront();
31
+ const t = useThemeStrings();
31
32
 
32
33
  if (!categories || categories.length === 0) {
33
34
  return null;
@@ -37,14 +38,14 @@ export function CategoryFilter({
37
38
  return (
38
39
  <nav
39
40
  className={`core-cat-tabs${className ? ` ${className}` : ''}`}
40
- aria-label="Blog categories"
41
+ aria-label={t('blog.filter.nav')}
41
42
  >
42
43
  <Link
43
44
  href={`${basePath}/blog`}
44
45
  className={`core-cat-tabs__item${!active ? ' is-active' : ''}`}
45
46
  aria-current={!active ? 'page' : undefined}
46
47
  >
47
- <span className="core-cat-tabs__label">All</span>
48
+ <span className="core-cat-tabs__label">{t('blog.filter.all')}</span>
48
49
  </Link>
49
50
  {categories.map((category) => {
50
51
  const isActive = active === category.slug;
@@ -67,13 +68,13 @@ export function CategoryFilter({
67
68
  }
68
69
 
69
70
  return (
70
- <nav className={`core-cat-filter${className ? ` ${className}` : ''}`} aria-label="Blog categories">
71
+ <nav className={`core-cat-filter${className ? ` ${className}` : ''}`} aria-label={t('blog.filter.nav')}>
71
72
  <Link
72
73
  href={`${basePath}/blog`}
73
74
  className={`core-cat-filter__item${!active ? ' is-active' : ''}`}
74
75
  aria-current={!active ? 'page' : undefined}
75
76
  >
76
- All
77
+ {t('blog.filter.all')}
77
78
  </Link>
78
79
  {categories.map((category) => {
79
80
  const isActive = active === category.slug;
@@ -1,9 +1,9 @@
1
1
  'use client';
2
2
 
3
+ import { useStorefront, useThemeStrings } from '../../provider';
3
4
  import type { JSX } from 'react';
4
5
  import Link from 'next/link';
5
6
  import type { Pagination } from '../../types/page';
6
- import { useStorefront } from '../../provider';
7
7
 
8
8
  /**
9
9
  * Shared blog pagination (Prev / "Page N" / Next). SSR page-based navigation
@@ -21,6 +21,7 @@ export function BlogPagination({
21
21
  className?: string;
22
22
  }): JSX.Element | null {
23
23
  const { basePath } = useStorefront();
24
+ const t = useThemeStrings();
24
25
 
25
26
  if (!pagination) {
26
27
  return null;
@@ -47,24 +48,24 @@ export function BlogPagination({
47
48
  };
48
49
 
49
50
  return (
50
- <nav className={`core-pagination${className ? ` ${className}` : ''}`} aria-label="Blog pagination">
51
+ <nav className={`core-pagination${className ? ` ${className}` : ''}`} aria-label={t('blog.pagination.nav')}>
51
52
  {hasPrev ? (
52
53
  <Link href={buildHref(current - 1)} className="core-pagination__link" rel="prev">
53
- ← Previous
54
+ {t('blog.pagination.prev')}
54
55
  </Link>
55
56
  ) : (
56
57
  <span className="core-pagination__link is-disabled" aria-disabled="true">
57
- ← Previous
58
+ {t('blog.pagination.prev')}
58
59
  </span>
59
60
  )}
60
- <span className="core-pagination__status">{`Page ${current}`}</span>
61
+ <span className="core-pagination__status">{t('blog.pagination.page', { current })}</span>
61
62
  {hasNext ? (
62
63
  <Link href={buildHref(current + 1)} className="core-pagination__link" rel="next">
63
- Next →
64
+ {t('blog.pagination.next')}
64
65
  </Link>
65
66
  ) : (
66
67
  <span className="core-pagination__link is-disabled" aria-disabled="true">
67
- Next →
68
+ {t('blog.pagination.next')}
68
69
  </span>
69
70
  )}
70
71
  </nav>
@@ -1,19 +1,25 @@
1
1
  'use client';
2
2
 
3
+ import { useStorefront, useThemeStrings } from '../../provider';
3
4
  import type { JSX } from 'react';
4
5
  import type { Post } from '../../types/page';
5
- import { useStorefront } from '../../provider';
6
6
  import { Image } from '../image';
7
7
  import { formatDisplayDate } from '../../utils/format';
8
+ import { resolveIntlLocale } from '../../utils/locale';
8
9
 
9
- function formatDate(value: string): string {
10
+ function formatDate(value: string, locale: string | null): string {
10
11
  const date = new Date(value);
11
12
  if (Number.isNaN(date.getTime())) {
12
13
  return '';
13
14
  }
14
- // en-US long form as before; the fixed zone keeps a post published near midnight
15
- // UTC on the same day on the server and in the browser.
16
- return formatDisplayDate(date, { year: 'numeric', month: 'long', day: 'numeric' }, 'en-US');
15
+ // Shopper's locale through the canonical mapping (English stays the
16
+ // British/Nigerian order, "15 February 2026"); the fixed zone keeps a post
17
+ // published near midnight UTC on the same day on the server and in the browser.
18
+ return formatDisplayDate(
19
+ date,
20
+ { year: 'numeric', month: 'long', day: 'numeric' },
21
+ resolveIntlLocale(locale),
22
+ );
17
23
  }
18
24
 
19
25
  /**
@@ -22,12 +28,13 @@ function formatDate(value: string): string {
22
28
  * style via `.core-post-meta*` selectors (colours inherit brand vars).
23
29
  */
24
30
  export function PostMeta({ post, className }: { post: Post; className?: string }): JSX.Element {
25
- const { vendor } = useStorefront();
31
+ const { vendor, locale } = useStorefront();
32
+ const t = useThemeStrings();
26
33
 
27
34
  const authorName = post.author?.name ?? vendor.name ?? null;
28
35
  const authorAvatar = post.author?.avatar_url ?? vendor.logo ?? null;
29
- const published = post.published_at ? formatDate(post.published_at) : '';
30
- const readingTime = post.reading_time && post.reading_time > 0 ? `${post.reading_time} min read` : '';
36
+ const published = post.published_at ? formatDate(post.published_at, locale) : '';
37
+ const readingTime = post.reading_time && post.reading_time > 0 ? t('blog.meta.reading', { count: post.reading_time }) : '';
31
38
 
32
39
  return (
33
40
  <div className={`core-post-meta${className ? ` ${className}` : ''}`}>
@@ -1,9 +1,9 @@
1
1
  'use client';
2
2
 
3
+ import { useStorefront, useThemeStrings } from '../../provider';
3
4
  import type { JSX } from 'react';
4
5
  import Link from 'next/link';
5
6
  import type { Post } from '../../types/page';
6
- import { useStorefront } from '../../provider';
7
7
  import { Image } from '../image';
8
8
 
9
9
  /**
@@ -13,13 +13,15 @@ import { Image } from '../image';
13
13
  export function RelatedPosts({
14
14
  posts,
15
15
  className,
16
- heading = 'Related posts',
16
+ heading,
17
17
  }: {
18
18
  posts?: Post[] | null;
19
19
  className?: string;
20
20
  heading?: string;
21
21
  }): JSX.Element | null {
22
22
  const { basePath } = useStorefront();
23
+ const t = useThemeStrings();
24
+ const title = heading ?? t('blog.related.title');
23
25
 
24
26
  if (!posts || posts.length === 0) {
25
27
  return null;
@@ -27,7 +29,7 @@ export function RelatedPosts({
27
29
 
28
30
  return (
29
31
  <section className={`core-related${className ? ` ${className}` : ''}`}>
30
- <h2 className="core-related__heading">{heading}</h2>
32
+ <h2 className="core-related__heading">{title}</h2>
31
33
  <div className="core-related__grid">
32
34
  {posts.map((post) => (
33
35
  <Link key={post.id ?? post.slug} href={`${basePath}/blog/${post.slug}`} className="core-related__card">
@@ -2,6 +2,7 @@
2
2
 
3
3
  import type { JSX } from 'react';
4
4
  import { useEffect, useState } from 'react';
5
+ import { useThemeStrings } from '../../provider';
5
6
 
6
7
  interface ShareButtonsProps {
7
8
  title: string;
@@ -18,6 +19,7 @@ export function ShareButtons({ title, className }: ShareButtonsProps): JSX.Eleme
18
19
  const [url, setUrl] = useState('');
19
20
  const [copied, setCopied] = useState(false);
20
21
  const [canNativeShare, setCanNativeShare] = useState(false);
22
+ const t = useThemeStrings();
21
23
 
22
24
  useEffect(() => {
23
25
  setUrl(window.location.href);
@@ -47,11 +49,11 @@ export function ShareButtons({ title, className }: ShareButtonsProps): JSX.Eleme
47
49
 
48
50
  return (
49
51
  <div className={`core-share${className ? ` ${className}` : ''}`}>
50
- <span className="core-share__label">Share</span>
52
+ <span className="core-share__label">{t('blog.share.label')}</span>
51
53
  <div className="core-share__actions">
52
54
  {canNativeShare ? (
53
- <button type="button" className="core-share__btn" onClick={nativeShare} aria-label="Share">
54
- Share
55
+ <button type="button" className="core-share__btn" onClick={nativeShare} aria-label={t('blog.share.action')}>
56
+ {t('blog.share.action')}
55
57
  </button>
56
58
  ) : null}
57
59
  <a
@@ -59,7 +61,7 @@ export function ShareButtons({ title, className }: ShareButtonsProps): JSX.Eleme
59
61
  href={`https://x.com/intent/post?url=${encodedUrl}&text=${encodedTitle}`}
60
62
  target="_blank"
61
63
  rel="noopener noreferrer"
62
- aria-label="Share on X"
64
+ aria-label={t('blog.share.x')}
63
65
  >
64
66
  X
65
67
  </a>
@@ -68,7 +70,7 @@ export function ShareButtons({ title, className }: ShareButtonsProps): JSX.Eleme
68
70
  href={`https://www.facebook.com/sharer/sharer.php?u=${encodedUrl}`}
69
71
  target="_blank"
70
72
  rel="noopener noreferrer"
71
- aria-label="Share on Facebook"
73
+ aria-label={t('blog.share.facebook')}
72
74
  >
73
75
  Facebook
74
76
  </a>
@@ -77,12 +79,12 @@ export function ShareButtons({ title, className }: ShareButtonsProps): JSX.Eleme
77
79
  href={`https://wa.me/?text=${encodedTitle}%20${encodedUrl}`}
78
80
  target="_blank"
79
81
  rel="noopener noreferrer"
80
- aria-label="Share on WhatsApp"
82
+ aria-label={t('blog.share.whatsapp')}
81
83
  >
82
84
  WhatsApp
83
85
  </a>
84
- <button type="button" className="core-share__btn" onClick={copyLink} aria-label="Copy link">
85
- {copied ? 'Copied' : 'Copy link'}
86
+ <button type="button" className="core-share__btn" onClick={copyLink} aria-label={t('blog.share.copy')}>
87
+ {copied ? t('blog.share.copied') : t('blog.share.copy')}
86
88
  </button>
87
89
  </div>
88
90
  </div>
@@ -3,16 +3,18 @@
3
3
  import type { JSX } from 'react';
4
4
  import { useEffect, useRef, useState } from 'react';
5
5
  import { useAuth } from '../../hooks/use-auth';
6
+ import { useThemeStrings } from '../../provider';
6
7
  import { Image } from '../image';
7
8
 
8
9
  /**
9
10
  * First section of the checkout form — shows who's checking out. Ordering
10
- * requires auth (`canCheckout` in checkout-controller.tsx), so unlike
11
- * Shopify's guest-friendly email field this is purely an identity
12
- * confirmation: avatar + email when signed in, a sign-in prompt otherwise.
11
+ * requires auth (`canCheckout` in checkout-controller.tsx), so this is purely
12
+ * an identity confirmation, not an email field: avatar + email when signed in,
13
+ * a sign-in prompt otherwise.
13
14
  */
14
15
  export function CheckoutContactSection(): JSX.Element {
15
16
  const { user, isAuthenticated, openAuthModal, logout } = useAuth();
17
+ const t = useThemeStrings();
16
18
  const [menuOpen, setMenuOpen] = useState(false);
17
19
  const menuRef = useRef<HTMLDivElement>(null);
18
20
 
@@ -31,13 +33,13 @@ export function CheckoutContactSection(): JSX.Element {
31
33
  return (
32
34
  <section className="core-checkout__section core-checkout-contact">
33
35
  <div className="core-checkout-contact__row">
34
- <span className="core-checkout-contact__label">Contact</span>
36
+ <span className="core-checkout-contact__label">{t('checkout.contact.label')}</span>
35
37
  <button
36
38
  type="button"
37
39
  className="core-checkout-contact__signin"
38
40
  onClick={() => openAuthModal('login')}
39
41
  >
40
- Sign in
42
+ {t('checkout.contact.signin')}
41
43
  </button>
42
44
  </div>
43
45
  </section>
@@ -62,7 +64,7 @@ export function CheckoutContactSection(): JSX.Element {
62
64
  <button
63
65
  type="button"
64
66
  className="core-checkout-contact__menu-btn"
65
- aria-label="Account options"
67
+ aria-label={t('checkout.contact.options')}
66
68
  onClick={() => setMenuOpen((v) => !v)}
67
69
  >
68
70
  <svg width="18" height="18" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
@@ -74,7 +76,7 @@ export function CheckoutContactSection(): JSX.Element {
74
76
  {menuOpen ? (
75
77
  <div className="core-checkout-contact__menu">
76
78
  <button type="button" onClick={() => { setMenuOpen(false); logout(); }}>
77
- Sign out
79
+ {t('checkout.contact.signout')}
78
80
  </button>
79
81
  </div>
80
82
  ) : null}
@@ -5,6 +5,7 @@ import type { CheckoutShellProps } from '../../types/theme';
5
5
  import { formatMoney } from '../../utils/format';
6
6
  import { useHref } from '../../hooks/use-href';
7
7
  import { CheckoutContactSection } from './checkout-contact';
8
+ import { useThemeStrings } from '../../provider';
8
9
  import { PolicyLinks } from './policy-links';
9
10
 
10
11
  export function DefaultCheckoutShell({
@@ -29,14 +30,15 @@ export function DefaultCheckoutShell({
29
30
  policies,
30
31
  }: CheckoutShellProps): JSX.Element {
31
32
  const href = useHref();
33
+ const t = useThemeStrings();
32
34
 
33
35
  if (mode === 'page') {
34
36
  if (totalCount === 0) {
35
37
  return (
36
38
  <div className="core-checkout core-checkout--page">
37
39
  <div className="core-checkout__empty">
38
- <p>Your cart is empty.</p>
39
- <Link href={href('/')} className="core-checkout__back-link">Browse products</Link>
40
+ <p>{t('checkout.empty.message')}</p>
41
+ <Link href={href('/')} className="core-checkout__back-link">{t('checkout.empty.browse')}</Link>
40
42
  </div>
41
43
  </div>
42
44
  );
@@ -52,23 +54,23 @@ export function DefaultCheckoutShell({
52
54
  <svg viewBox="0 0 16 16" fill="none" aria-hidden="true" width="14" height="14">
53
55
  <path d="M10 13L5 8l5-5" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
54
56
  </svg>
55
- Continue shopping
57
+ {t('checkout.page.back')}
56
58
  </Link>
57
59
  </div>
58
60
  <CheckoutContactSection />
59
61
  <section className="core-checkout__section">
60
- <h3 className="core-checkout__section-title">How you&apos;ll get it</h3>
62
+ <h3 className="core-checkout__section-title">{t('checkout.delivery.title')}</h3>
61
63
  {deliveryModes}
62
64
  </section>
63
65
  {shippingZonePicker ? (
64
66
  <section className="core-checkout__section">
65
- <h3 className="core-checkout__section-title">Shipping zone</h3>
67
+ <h3 className="core-checkout__section-title">{t('checkout.zone.title')}</h3>
66
68
  {shippingZonePicker}
67
69
  </section>
68
70
  ) : null}
69
71
  {addressPicker ? (
70
72
  <section className="core-checkout__section">
71
- <h3 className="core-checkout__section-title">Delivery address</h3>
73
+ <h3 className="core-checkout__section-title">{t('checkout.address.title')}</h3>
72
74
  {addressPicker}
73
75
  </section>
74
76
  ) : null}
@@ -79,13 +81,13 @@ export function DefaultCheckoutShell({
79
81
  ) : null}
80
82
  {schedulePicker ? (
81
83
  <section className="core-checkout__section">
82
- <h3 className="core-checkout__section-title">Schedule</h3>
84
+ <h3 className="core-checkout__section-title">{t('checkout.schedule.title')}</h3>
83
85
  {schedulePicker}
84
86
  </section>
85
87
  ) : null}
86
88
  <section className="core-checkout__section">
87
- <h3 className="core-checkout__payment-title">Payment</h3>
88
- <p className="core-checkout__payment-subtitle">All transactions are secure and encrypted.</p>
89
+ <h3 className="core-checkout__payment-title">{t('checkout.payment.title')}</h3>
90
+ <p className="core-checkout__payment-subtitle">{t('checkout.payment.secure')}</p>
89
91
  {paymentPicker}
90
92
  </section>
91
93
  <section className="core-checkout__section">
@@ -97,7 +99,7 @@ export function DefaultCheckoutShell({
97
99
  {/* Right pane: off-white — order summary */}
98
100
  <div className="core-checkout__page-summary">
99
101
  <div className="core-checkout__summary-card">
100
- <h3 className="core-checkout__section-title">Order summary</h3>
102
+ <h3 className="core-checkout__section-title">{t('checkout.summary.title')}</h3>
101
103
  {cartItems}
102
104
  <div className="core-checkout__summary-promo">{promoInput}</div>
103
105
  <div className="core-checkout__summary-fees">{feesBreakdown}</div>
@@ -123,7 +125,7 @@ export function DefaultCheckoutShell({
123
125
  className={`core-checkout__tab${activeTab === 'summary' ? ' is-active' : ''}`}
124
126
  onClick={() => onTabChange('summary')}
125
127
  >
126
- Summary {totalCount > 0 ? `(${totalCount})` : ''}
128
+ {t('checkout.tabs.summary')}{totalCount > 0 ? ` (${totalCount})` : ' '}
127
129
  </button>
128
130
  <button
129
131
  type="button"
@@ -131,24 +133,24 @@ export function DefaultCheckoutShell({
131
133
  onClick={() => onTabChange('checkout')}
132
134
  disabled={totalCount === 0}
133
135
  >
134
- Checkout
136
+ {t('checkout.tabs.checkout')}
135
137
  </button>
136
138
  </div>
137
139
 
138
140
  {activeTab === 'summary' ? (
139
141
  <div className="core-checkout__summary">
140
142
  {totalCount === 0 ? (
141
- <div className="core-checkout__empty"><p>Your cart is empty</p></div>
143
+ <div className="core-checkout__empty"><p>{t('checkout.summary.empty')}</p></div>
142
144
  ) : (
143
145
  <>
144
146
  {cartItems}
145
147
  <div className="core-checkout__summary-footer">
146
148
  <div className="core-checkout__subtotal">
147
- <span>Subtotal</span>
149
+ <span>{t('checkout.summary.subtotal')}</span>
148
150
  <span>{formatMoney(subtotal, currency)}</span>
149
151
  </div>
150
152
  <button type="button" className="core-submit-btn" onClick={() => onTabChange('checkout')}>
151
- Proceed to checkout
153
+ {t('checkout.summary.proceed')}
152
154
  </button>
153
155
  </div>
154
156
  </>
@@ -6,6 +6,7 @@ import { createPortal } from 'react-dom';
6
6
  import Link from 'next/link';
7
7
  import type { Page } from '../../types/page';
8
8
  import { useHref } from '../../hooks/use-href';
9
+ import { useThemeStrings } from '../../provider';
9
10
  import { usePolicyModalStore } from '../../stores/policy-modal-store';
10
11
  import { PageRenderer } from '../../page-renderer';
11
12
 
@@ -23,6 +24,7 @@ function subscribeNoop(): () => void {
23
24
  }
24
25
 
25
26
  export function PolicyLinks({ policies }: { policies: Page[] }): JSX.Element | null {
27
+ const t = useThemeStrings();
26
28
  const activeSlug = usePolicyModalStore((s) => s.activeSlug);
27
29
  const open = usePolicyModalStore((s) => s.open);
28
30
  const close = usePolicyModalStore((s) => s.close);
@@ -59,7 +61,7 @@ export function PolicyLinks({ policies }: { policies: Page[] }): JSX.Element | n
59
61
  >
60
62
  <div className="core-policy-modal__head">
61
63
  <h2 className="core-policy-modal__title">{active.title}</h2>
62
- <button type="button" className="core-policy-modal__close" aria-label="Close" onClick={close}>
64
+ <button type="button" className="core-policy-modal__close" aria-label={t('checkout.policy.close')} onClick={close}>
63
65
  <svg width="18" height="18" viewBox="0 0 20 20" fill="none" aria-hidden="true">
64
66
  <path d="M5 5l10 10M15 5L5 15" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" />
65
67
  </svg>
@@ -70,7 +72,7 @@ export function PolicyLinks({ policies }: { policies: Page[] }): JSX.Element | n
70
72
  </div>
71
73
  <div className="core-policy-modal__foot">
72
74
  <Link href={href(`/${active.slug}`)} className="core-policy-modal__full-link" onClick={close}>
73
- View full page
75
+ {t('checkout.policy.view')}
74
76
  </Link>
75
77
  </div>
76
78
  </div>
@@ -81,7 +83,7 @@ export function PolicyLinks({ policies }: { policies: Page[] }): JSX.Element | n
81
83
 
82
84
  return (
83
85
  <>
84
- <nav className="core-checkout-policies" aria-label="Store policies">
86
+ <nav className="core-checkout-policies" aria-label={t('checkout.policy.list')}>
85
87
  {policies.map((policy) => (
86
88
  <button
87
89
  key={policy.id}
@@ -35,11 +35,11 @@ type ImageProps = ImgHTMLAttributes<HTMLImageElement> & {
35
35
  fallback?: string;
36
36
  placeholder?: PlaceholderProp;
37
37
  /**
38
- * The backend's srcset-ready tier map (product.media.image_variants,
38
+ * The API's srcset-ready tier map (product.media.image_variants,
39
39
  * vendor.logo_variants, collection.image_variants, mediaIndex[url], …).
40
- * Purely additive: when absent — as it is at every call site that hasn't been
41
- * migrated, and whenever the backend legitimately sends null (off-host image,
42
- * SVG, legacy upload) — this component behaves exactly as it did before.
40
+ * Purely additive: when absent — and whenever the API legitimately sends null
41
+ * (off-host image, SVG, legacy upload) — this component renders the plain
42
+ * image URL.
43
43
  */
44
44
  variants?: ImageVariants | null;
45
45
  /**
@@ -133,7 +133,7 @@ export function Image({ fallback, placeholder, variants, intent = 'raw', onError
133
133
  // image is invisible until hover. Eager, a server-rendered grid of 24 cards
134
134
  // made React preload the first ~10 (also into the 103 Early Hints header) and
135
135
  // fetch every image at once, racing the page's render-blocking CSS: /shop's
136
- // first paint went 1.8 s → 2.9 s on Slow 4G (24/9/26). Lazy is next/image's
136
+ // first paint went from 1.8 s to 2.9 s on Slow 4G. Lazy is next/image's
137
137
  // default as well. A theme whose card IS the page's main image passes
138
138
  // loading="eager" or fetchPriority="high".
139
139
  const lazyByDefault = (intent === 'card' || intent === 'card2x') && props.fetchPriority !== 'high';