create-magic-storefront 0.1.1 → 0.1.3

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 (109) hide show
  1. package/package.json +1 -1
  2. package/template/.claude/skills/storefront-design/SKILL.md +19 -8
  3. package/template/.claude/skills/storefront-verify/SKILL.md +85 -0
  4. package/template/.claude/skills/vercel-react-best-practices/AGENTS.md +3810 -0
  5. package/template/.claude/skills/vercel-react-best-practices/README.md +123 -0
  6. package/template/.claude/skills/vercel-react-best-practices/SKILL.md +149 -0
  7. package/template/.claude/skills/vercel-react-best-practices/SOURCE.md +7 -0
  8. package/template/.claude/skills/vercel-react-best-practices/metadata.json +15 -0
  9. package/template/.claude/skills/vercel-react-best-practices/rules/_sections.md +46 -0
  10. package/template/.claude/skills/vercel-react-best-practices/rules/_template.md +28 -0
  11. package/template/.claude/skills/vercel-react-best-practices/rules/advanced-effect-event-deps.md +56 -0
  12. package/template/.claude/skills/vercel-react-best-practices/rules/advanced-event-handler-refs.md +55 -0
  13. package/template/.claude/skills/vercel-react-best-practices/rules/advanced-init-once.md +42 -0
  14. package/template/.claude/skills/vercel-react-best-practices/rules/advanced-use-latest.md +39 -0
  15. package/template/.claude/skills/vercel-react-best-practices/rules/async-api-routes.md +38 -0
  16. package/template/.claude/skills/vercel-react-best-practices/rules/async-cheap-condition-before-await.md +37 -0
  17. package/template/.claude/skills/vercel-react-best-practices/rules/async-defer-await.md +82 -0
  18. package/template/.claude/skills/vercel-react-best-practices/rules/async-dependencies.md +51 -0
  19. package/template/.claude/skills/vercel-react-best-practices/rules/async-parallel.md +28 -0
  20. package/template/.claude/skills/vercel-react-best-practices/rules/async-suspense-boundaries.md +99 -0
  21. package/template/.claude/skills/vercel-react-best-practices/rules/bundle-analyzable-paths.md +63 -0
  22. package/template/.claude/skills/vercel-react-best-practices/rules/bundle-barrel-imports.md +60 -0
  23. package/template/.claude/skills/vercel-react-best-practices/rules/bundle-conditional.md +31 -0
  24. package/template/.claude/skills/vercel-react-best-practices/rules/bundle-defer-third-party.md +49 -0
  25. package/template/.claude/skills/vercel-react-best-practices/rules/bundle-dynamic-imports.md +35 -0
  26. package/template/.claude/skills/vercel-react-best-practices/rules/bundle-preload.md +50 -0
  27. package/template/.claude/skills/vercel-react-best-practices/rules/client-event-listeners.md +74 -0
  28. package/template/.claude/skills/vercel-react-best-practices/rules/client-localstorage-schema.md +71 -0
  29. package/template/.claude/skills/vercel-react-best-practices/rules/client-passive-event-listeners.md +48 -0
  30. package/template/.claude/skills/vercel-react-best-practices/rules/client-swr-dedup.md +56 -0
  31. package/template/.claude/skills/vercel-react-best-practices/rules/js-batch-dom-css.md +107 -0
  32. package/template/.claude/skills/vercel-react-best-practices/rules/js-cache-function-results.md +80 -0
  33. package/template/.claude/skills/vercel-react-best-practices/rules/js-cache-property-access.md +28 -0
  34. package/template/.claude/skills/vercel-react-best-practices/rules/js-cache-storage.md +70 -0
  35. package/template/.claude/skills/vercel-react-best-practices/rules/js-combine-iterations.md +32 -0
  36. package/template/.claude/skills/vercel-react-best-practices/rules/js-early-exit.md +50 -0
  37. package/template/.claude/skills/vercel-react-best-practices/rules/js-flatmap-filter.md +60 -0
  38. package/template/.claude/skills/vercel-react-best-practices/rules/js-hoist-regexp.md +45 -0
  39. package/template/.claude/skills/vercel-react-best-practices/rules/js-index-maps.md +37 -0
  40. package/template/.claude/skills/vercel-react-best-practices/rules/js-length-check-first.md +49 -0
  41. package/template/.claude/skills/vercel-react-best-practices/rules/js-min-max-loop.md +82 -0
  42. package/template/.claude/skills/vercel-react-best-practices/rules/js-request-idle-callback.md +105 -0
  43. package/template/.claude/skills/vercel-react-best-practices/rules/js-set-map-lookups.md +24 -0
  44. package/template/.claude/skills/vercel-react-best-practices/rules/js-tosorted-immutable.md +57 -0
  45. package/template/.claude/skills/vercel-react-best-practices/rules/rendering-activity.md +26 -0
  46. package/template/.claude/skills/vercel-react-best-practices/rules/rendering-animate-svg-wrapper.md +47 -0
  47. package/template/.claude/skills/vercel-react-best-practices/rules/rendering-conditional-render.md +40 -0
  48. package/template/.claude/skills/vercel-react-best-practices/rules/rendering-content-visibility.md +38 -0
  49. package/template/.claude/skills/vercel-react-best-practices/rules/rendering-hoist-jsx.md +46 -0
  50. package/template/.claude/skills/vercel-react-best-practices/rules/rendering-hydration-no-flicker.md +82 -0
  51. package/template/.claude/skills/vercel-react-best-practices/rules/rendering-hydration-suppress-warning.md +30 -0
  52. package/template/.claude/skills/vercel-react-best-practices/rules/rendering-resource-hints.md +85 -0
  53. package/template/.claude/skills/vercel-react-best-practices/rules/rendering-script-defer-async.md +68 -0
  54. package/template/.claude/skills/vercel-react-best-practices/rules/rendering-svg-precision.md +28 -0
  55. package/template/.claude/skills/vercel-react-best-practices/rules/rendering-usetransition-loading.md +75 -0
  56. package/template/.claude/skills/vercel-react-best-practices/rules/rerender-defer-reads.md +39 -0
  57. package/template/.claude/skills/vercel-react-best-practices/rules/rerender-dependencies.md +45 -0
  58. package/template/.claude/skills/vercel-react-best-practices/rules/rerender-derived-state-no-effect.md +40 -0
  59. package/template/.claude/skills/vercel-react-best-practices/rules/rerender-derived-state.md +29 -0
  60. package/template/.claude/skills/vercel-react-best-practices/rules/rerender-functional-setstate.md +74 -0
  61. package/template/.claude/skills/vercel-react-best-practices/rules/rerender-lazy-state-init.md +58 -0
  62. package/template/.claude/skills/vercel-react-best-practices/rules/rerender-memo-with-default-value.md +38 -0
  63. package/template/.claude/skills/vercel-react-best-practices/rules/rerender-memo.md +44 -0
  64. package/template/.claude/skills/vercel-react-best-practices/rules/rerender-move-effect-to-event.md +45 -0
  65. package/template/.claude/skills/vercel-react-best-practices/rules/rerender-no-inline-components.md +82 -0
  66. package/template/.claude/skills/vercel-react-best-practices/rules/rerender-simple-expression-in-memo.md +35 -0
  67. package/template/.claude/skills/vercel-react-best-practices/rules/rerender-split-combined-hooks.md +64 -0
  68. package/template/.claude/skills/vercel-react-best-practices/rules/rerender-transitions.md +40 -0
  69. package/template/.claude/skills/vercel-react-best-practices/rules/rerender-use-deferred-value.md +59 -0
  70. package/template/.claude/skills/vercel-react-best-practices/rules/rerender-use-ref-transient-values.md +73 -0
  71. package/template/.claude/skills/vercel-react-best-practices/rules/server-after-nonblocking.md +73 -0
  72. package/template/.claude/skills/vercel-react-best-practices/rules/server-auth-actions.md +96 -0
  73. package/template/.claude/skills/vercel-react-best-practices/rules/server-cache-lru.md +41 -0
  74. package/template/.claude/skills/vercel-react-best-practices/rules/server-cache-react.md +76 -0
  75. package/template/.claude/skills/vercel-react-best-practices/rules/server-dedup-props.md +65 -0
  76. package/template/.claude/skills/vercel-react-best-practices/rules/server-hoist-static-io.md +149 -0
  77. package/template/.claude/skills/vercel-react-best-practices/rules/server-no-shared-module-state.md +50 -0
  78. package/template/.claude/skills/vercel-react-best-practices/rules/server-parallel-fetching.md +83 -0
  79. package/template/.claude/skills/vercel-react-best-practices/rules/server-parallel-nested-fetching.md +34 -0
  80. package/template/.claude/skills/vercel-react-best-practices/rules/server-serialization.md +38 -0
  81. package/template/.claude/skills/web-interface-guidelines/LICENSE +21 -0
  82. package/template/.claude/skills/web-interface-guidelines/SKILL.md +40 -0
  83. package/template/.claude/skills/web-interface-guidelines/SOURCE.md +9 -0
  84. package/template/.claude/skills/web-interface-guidelines/guidelines.md +155 -0
  85. package/template/AGENTS.md +19 -12
  86. package/template/PAGES.md +25 -2
  87. package/template/README.md +1 -1
  88. package/template/_gitignore +1 -0
  89. package/template/app/account/page.tsx +31 -11
  90. package/template/app/cart/page.tsx +50 -54
  91. package/template/app/checkout/page.tsx +86 -38
  92. package/template/app/collections/[handle]/page.tsx +3 -2
  93. package/template/app/error.tsx +6 -4
  94. package/template/app/globals.css +29 -0
  95. package/template/app/layout.tsx +4 -2
  96. package/template/app/not-found.tsx +5 -2
  97. package/template/app/pages/[handle]/page.tsx +3 -2
  98. package/template/app/search/page.tsx +11 -6
  99. package/template/components/buy-box.tsx +7 -4
  100. package/template/components/cart-link.tsx +8 -2
  101. package/template/components/pager.tsx +3 -1
  102. package/template/components/product-grid.tsx +9 -5
  103. package/template/components/sections/banner.tsx +7 -1
  104. package/template/components/sections/deal-of-day.tsx +16 -5
  105. package/template/components/sections/product-shelves.tsx +8 -7
  106. package/template/components/sections/store-reviews.tsx +7 -5
  107. package/template/lib/errors.ts +1 -4
  108. package/template/lib/i18n.ts +315 -0
  109. package/template/llms.txt +8 -6
@@ -1,17 +1,19 @@
1
1
  'use client';
2
2
 
3
- import { MagicStoreError } from '@magicstoreai/hydrogen';
3
+ import { MagicStoreError, useMagicStore } from '@magicstoreai/hydrogen';
4
+ import { messages } from '@/lib/i18n';
4
5
 
5
6
  /** Any failure a page did not handle itself: the API's detail when there is one, and a retry. */
6
7
  export default function ErrorPage({ error, reset }: { error: Error; reset: () => void }) {
8
+ const t = messages(useMagicStore().locale);
7
9
  const unavailable = error instanceof MagicStoreError && error.code === 'STORE_UNAVAILABLE';
8
10
  return (
9
11
  <div className="stack">
10
- <h1>{unavailable ? 'The shop is temporarily closed' : 'Something went wrong'}</h1>
12
+ <h1>{unavailable ? t.shopClosed : t.somethingWentWrong}</h1>
11
13
  {error instanceof MagicStoreError && error.detail && <p>{error.detail}</p>}
12
14
  <div className="row">
13
- <button onClick={reset}>Try again</button>
14
- <a href="/">Back to the shop</a>
15
+ <button onClick={reset}>{t.tryAgain}</button>
16
+ <a href="/">{t.backToShop}</a>
15
17
  </div>
16
18
  </div>
17
19
  );
@@ -150,6 +150,35 @@ th {
150
150
  text-align: left;
151
151
  vertical-align: middle;
152
152
  }
153
+ .cart-lines {
154
+ list-style: none;
155
+ margin: 0;
156
+ padding: 0;
157
+ }
158
+ /* Phone: image and title on one row, the controls under the title. Wider: all on one row. */
159
+ .cart-line {
160
+ display: grid;
161
+ grid-template-columns: 64px minmax(0, 1fr);
162
+ gap: 8px 12px;
163
+ padding: 12px 0;
164
+ border-bottom: 1px solid var(--line);
165
+ }
166
+ .cart-line-controls {
167
+ grid-column: 2;
168
+ display: flex;
169
+ gap: 12px;
170
+ align-items: center;
171
+ flex-wrap: wrap;
172
+ }
173
+ @media (min-width: 720px) {
174
+ .cart-line {
175
+ grid-template-columns: 64px minmax(0, 1fr) auto;
176
+ align-items: center;
177
+ }
178
+ .cart-line-controls {
179
+ grid-column: auto;
180
+ }
181
+ }
153
182
  .pager {
154
183
  display: flex;
155
184
  gap: 8px;
@@ -5,6 +5,7 @@ import { Image } from '@magicstoreai/hydrogen';
5
5
  import { CartLink } from '@/components/cart-link';
6
6
  import { api, locale } from '@/lib/api';
7
7
  import { brandStyle } from '@/lib/brand';
8
+ import { messages } from '@/lib/i18n';
8
9
  import { Providers } from './providers';
9
10
  import './globals.css';
10
11
 
@@ -27,6 +28,7 @@ export default async function RootLayout({ children }: { children: ReactNode })
27
28
  api.collectionsIndex({ query: { perPage: 6 } }),
28
29
  api.pagesIndex(),
29
30
  ]);
31
+ const t = messages(locale);
30
32
 
31
33
  return (
32
34
  // The merchant's colors, read per request: a change in the admin shows after SHOP_UPDATED.
@@ -47,8 +49,8 @@ export default async function RootLayout({ children }: { children: ReactNode })
47
49
  {collection.title}
48
50
  </Link>
49
51
  ))}
50
- <Link href="/search">Search</Link>
51
- <Link href="/account">Account</Link>
52
+ <Link href="/search">{t.search}</Link>
53
+ <Link href="/account">{t.account}</Link>
52
54
  <CartLink />
53
55
  </nav>
54
56
  </header>
@@ -1,10 +1,13 @@
1
1
  import Link from 'next/link';
2
+ import { locale } from '@/lib/api';
3
+ import { messages } from '@/lib/i18n';
2
4
 
3
5
  export default function NotFound() {
6
+ const t = messages(locale);
4
7
  return (
5
8
  <div className="stack">
6
- <h1>Not found</h1>
7
- <Link href="/">Back to the shop</Link>
9
+ <h1>{t.notFound}</h1>
10
+ <Link href="/">{t.backToShop}</Link>
8
11
  </div>
9
12
  );
10
13
  }
@@ -1,6 +1,7 @@
1
1
  import type { Metadata } from 'next';
2
2
  import { pageMeta } from '@magicstoreai/hydrogen/seo';
3
- import { api, orNotFound } from '@/lib/api';
3
+ import { api, locale, orNotFound } from '@/lib/api';
4
+ import { messages } from '@/lib/i18n';
4
5
 
5
6
  type Props = { params: Promise<{ handle: string }> };
6
7
 
@@ -21,7 +22,7 @@ export default async function ContentPage({ params }: Props) {
21
22
  {page.body ? (
22
23
  <div dangerouslySetInnerHTML={{ __html: page.body }} />
23
24
  ) : (
24
- <p className="muted">Nothing here yet.</p>
25
+ <p className="muted">{messages(locale).nothingHere}</p>
25
26
  )}
26
27
  </article>
27
28
  );
@@ -2,29 +2,34 @@ import type { Metadata } from 'next';
2
2
  import { SearchView } from '@/components/analytics-views';
3
3
  import { Pager } from '@/components/pager';
4
4
  import { ProductGrid } from '@/components/product-grid';
5
- import { api } from '@/lib/api';
5
+ import { api, locale } from '@/lib/api';
6
+ import { messages } from '@/lib/i18n';
6
7
 
7
8
  type Props = { searchParams: Promise<{ q?: string; page?: string }> };
8
9
 
9
10
  // Result pages are endless and change by the minute: keep them out of the index.
10
- export const metadata: Metadata = { title: 'Search', robots: { index: false, follow: true } };
11
+ export const metadata: Metadata = {
12
+ title: messages(locale).search,
13
+ robots: { index: false, follow: true },
14
+ };
11
15
 
12
16
  export default async function SearchPage({ searchParams }: Props) {
13
17
  const { q = '', page: pageParam } = await searchParams;
14
18
  const page = Math.max(1, Number(pageParam) || 1);
19
+ const t = messages(locale);
15
20
  const results = q.trim() ? await api.search({ query: { q, page, perPage: 24 } }) : null;
16
21
 
17
22
  return (
18
23
  <>
19
- <h1>Search</h1>
24
+ <h1>{t.search}</h1>
20
25
  <form className="row" action="/search">
21
- <input name="q" defaultValue={q} placeholder="What are you looking for?" />
22
- <button className="primary">Search</button>
26
+ <input name="q" defaultValue={q} placeholder={t.searchPlaceholder} />
27
+ <button className="primary">{t.search}</button>
23
28
  </form>
24
29
  {results && (
25
30
  <>
26
31
  <SearchView query={q} resultsCount={results.meta.pagination.total} />
27
- <p className="muted">{results.meta.pagination.total} found</p>
32
+ <p className="muted">{t.found(results.meta.pagination.total)}</p>
28
33
  <ProductGrid products={results.data} />
29
34
  <Pager
30
35
  meta={results.meta.pagination}
@@ -5,17 +5,20 @@ import {
5
5
  Money,
6
6
  useAnalytics,
7
7
  useCart,
8
+ useMagicStore,
8
9
  useVariantSelection,
9
10
  useWishlist,
10
11
  } from '@magicstoreai/hydrogen';
11
12
  import type { Schema } from '@magicstoreai/storefront-client';
12
13
  import { useEffect, useState } from 'react';
14
+ import { messages } from '@/lib/i18n';
13
15
 
14
16
  export function BuyBox({ product }: { product: Schema<'ProductDetail'> }) {
15
17
  const { selectedOptions, selectedVariant, setOption, isAvailable } = useVariantSelection(product);
16
18
  const { addLine, status } = useCart();
17
19
  const wishlist = useWishlist();
18
20
  const analytics = useAnalytics();
21
+ const t = messages(useMagicStore().locale);
19
22
  const [error, setError] = useState<string | null>(null);
20
23
  const [added, setAdded] = useState(false);
21
24
 
@@ -37,7 +40,7 @@ export function BuyBox({ product }: { product: Schema<'ProductDetail'> }) {
37
40
  await addLine({ productId: product.id, variantId, quantity: 1 });
38
41
  setAdded(true);
39
42
  } catch (e) {
40
- setError(e instanceof MagicStoreError ? (e.detail ?? e.message) : 'Could not add.');
43
+ setError(e instanceof MagicStoreError ? (e.detail ?? e.message) : t.couldNotAdd);
41
44
  }
42
45
  }
43
46
 
@@ -73,13 +76,13 @@ export function BuyBox({ product }: { product: Schema<'ProductDetail'> }) {
73
76
  ))}
74
77
  <div className="row">
75
78
  <button className="primary" disabled={!forSale || status === 'updating'} onClick={add}>
76
- {forSale ? 'Add to cart' : 'Sold out'}
79
+ {forSale ? t.addToCart : t.soldOut}
77
80
  </button>
78
81
  <button type="button" onClick={() => void wishlist.toggle(product.id)}>
79
- {wishlist.has(product.id) ? '♥ Saved' : '♡ Save'}
82
+ {wishlist.has(product.id) ? `♥ ${t.saved}` : `♡ ${t.save}`}
80
83
  </button>
81
84
  </div>
82
- {added && <p className="muted">Added to cart.</p>}
85
+ {added && <p className="muted">{t.addedToCart}</p>}
83
86
  {error && <p className="error">{error}</p>}
84
87
  </div>
85
88
  );
@@ -1,9 +1,15 @@
1
1
  'use client';
2
2
 
3
- import { useCart } from '@magicstoreai/hydrogen';
3
+ import { useCart, useMagicStore } from '@magicstoreai/hydrogen';
4
4
  import Link from 'next/link';
5
+ import { messages } from '@/lib/i18n';
5
6
 
6
7
  export function CartLink() {
7
8
  const { totalQuantity } = useCart();
8
- return <Link href="/cart">Cart ({totalQuantity})</Link>;
9
+ const t = messages(useMagicStore().locale);
10
+ return (
11
+ <Link href="/cart">
12
+ {t.cart} ({totalQuantity})
13
+ </Link>
14
+ );
9
15
  }
@@ -1,5 +1,7 @@
1
1
  import { paginationState, type Pagination } from '@magicstoreai/hydrogen';
2
2
  import Link from 'next/link';
3
+ import { locale } from '@/lib/api';
4
+ import { messages } from '@/lib/i18n';
3
5
 
4
6
  /** Server-rendered page links (`paginationState` is plain arithmetic, no hooks). */
5
7
  export function Pager({ meta, href }: { meta: Pagination; href: (page: number) => string }) {
@@ -8,7 +10,7 @@ export function Pager({ meta, href }: { meta: Pagination; href: (page: number) =
8
10
  }
9
11
  const state = paginationState(meta);
10
12
  return (
11
- <nav className="pager" aria-label="Pages">
13
+ <nav className="pager" aria-label={messages(locale).pages}>
12
14
  {state.pages.map((page, index) =>
13
15
  page === null ? (
14
16
  <span key={`gap-${index}`}>…</span>
@@ -1,13 +1,16 @@
1
1
  import { Image, Money } from '@magicstoreai/hydrogen';
2
2
  import type { Schema } from '@magicstoreai/storefront-client';
3
3
  import Link from 'next/link';
4
+ import { locale } from '@/lib/api';
5
+ import { messages } from '@/lib/i18n';
4
6
 
5
7
  /** Badges come only from the product's data; at most two, so the card stays readable. */
6
8
  function ProductBadges({ product }: { product: Schema<'Product'> }) {
9
+ const t = messages(locale);
7
10
  const badges = [
8
- product.preOrder && 'Pre-order',
9
- product.compareAtPrice && 'Sale',
10
- product.isNew && 'New',
11
+ product.preOrder && t.preOrder,
12
+ product.compareAtPrice && t.sale,
13
+ product.isNew && t.isNew,
11
14
  ]
12
15
  .filter((badge): badge is string => typeof badge === 'string')
13
16
  .slice(0, 2);
@@ -26,8 +29,9 @@ function ProductBadges({ product }: { product: Schema<'Product'> }) {
26
29
  }
27
30
 
28
31
  export function ProductGrid({ products }: { products: Schema<'Product'>[] }) {
32
+ const t = messages(locale);
29
33
  if (products.length === 0) {
30
- return <p className="muted">Nothing here yet.</p>;
34
+ return <p className="muted">{t.nothingHere}</p>;
31
35
  }
32
36
  return (
33
37
  <div className="grid">
@@ -48,7 +52,7 @@ export function ProductGrid({ products }: { products: Schema<'Product'>[] }) {
48
52
  </s>
49
53
  )}
50
54
  </span>
51
- {!product.availableForSale && <span className="muted">Sold out</span>}
55
+ {!product.availableForSale && <span className="muted">{t.soldOut}</span>}
52
56
  </Link>
53
57
  ))}
54
58
  </div>
@@ -1,5 +1,7 @@
1
1
  import { Image } from '@magicstoreai/hydrogen';
2
2
  import Link from 'next/link';
3
+ import { locale } from '@/lib/api';
4
+ import { messages } from '@/lib/i18n';
3
5
 
4
6
  import type { SectionOf } from './types';
5
7
 
@@ -25,7 +27,11 @@ export function Banner({ section }: { section: SectionOf<'BANNER'> }) {
25
27
  <section className="banner">
26
28
  {section.settings.slides.map((slide, index) => {
27
29
  const image = (
28
- <Image data={slide.image} loading={index === 0 ? 'eager' : 'lazy'} alt="Banner" />
30
+ <Image
31
+ data={slide.image}
32
+ loading={index === 0 ? 'eager' : 'lazy'}
33
+ alt={messages(locale).banner}
34
+ />
29
35
  );
30
36
  const href = slideHref(slide.link);
31
37
  return href === null ? (
@@ -1,9 +1,12 @@
1
1
  import { Image, Money } from '@magicstoreai/hydrogen';
2
2
  import Link from 'next/link';
3
+ import { api, locale } from '@/lib/api';
4
+ import { messages } from '@/lib/i18n';
3
5
 
4
6
  import type { SectionOf } from './types';
5
7
 
6
- export function DealOfDay({ section }: { section: SectionOf<'DEAL_OF_DAY'> }) {
8
+ export async function DealOfDay({ section }: { section: SectionOf<'DEAL_OF_DAY'> }) {
9
+ const t = messages(locale);
7
10
  const { product, image, price, compareAtPrice, endsAt, accentColor, badgeText, stock } =
8
11
  section.settings;
9
12
  return (
@@ -31,18 +34,26 @@ export function DealOfDay({ section }: { section: SectionOf<'DEAL_OF_DAY'> }) {
31
34
  {/* The merchant's "N of M left" bar — not the product's real stock. */}
32
35
  {stock && (
33
36
  <label className="stack">
34
- <span className="muted">
35
- {stock.remaining} of {stock.initial} left
36
- </span>
37
+ <span className="muted">{t.stockLeft(stock.remaining, stock.initial)}</span>
37
38
  <progress value={stock.remaining} max={Math.max(stock.initial, stock.remaining, 1)} />
38
39
  </label>
39
40
  )}
40
41
  {endsAt && (
41
42
  <span className="muted">
42
- Ends <time dateTime={endsAt}>{new Date(endsAt).toUTCString()}</time>
43
+ {t.endsAt} <time dateTime={endsAt}>{await endsAtText(endsAt)}</time>
43
44
  </span>
44
45
  )}
45
46
  </div>
46
47
  </section>
47
48
  );
48
49
  }
50
+
51
+ /** The end of the deal in the shop's time zone, as the storefront's language writes dates. */
52
+ async function endsAtText(endsAt: string): Promise<string> {
53
+ const { data: shop } = await api.shop();
54
+ return new Intl.DateTimeFormat(locale, {
55
+ dateStyle: 'medium',
56
+ timeStyle: 'short',
57
+ timeZone: shop.timezone,
58
+ }).format(new Date(endsAt));
59
+ }
@@ -3,7 +3,8 @@ import Link from 'next/link';
3
3
  import type { ReactNode } from 'react';
4
4
 
5
5
  import { ProductGrid } from '@/components/product-grid';
6
- import { api } from '@/lib/api';
6
+ import { api, locale } from '@/lib/api';
7
+ import { messages } from '@/lib/i18n';
7
8
 
8
9
  import { SHELF_SIZE, type SectionOf } from './types';
9
10
 
@@ -27,7 +28,7 @@ function Shelf({
27
28
  <div className="row">
28
29
  <h2>{title}</h2>
29
30
  {children}
30
- {href && <Link href={href}>See all</Link>}
31
+ {href && <Link href={href}>{messages(locale).seeAll}</Link>}
31
32
  </div>
32
33
  <ProductGrid products={products} />
33
34
  </section>
@@ -42,35 +43,35 @@ export async function NewArrivals({ section }: { section: ShelfSection }) {
42
43
  const { data } = await api.productsIndex({
43
44
  query: { sort: '-createdAt', perPage: section.settings.limit ?? SHELF_SIZE },
44
45
  });
45
- return <Shelf title="New arrivals" products={data} />;
46
+ return <Shelf title={messages(locale).newArrivals} products={data} />;
46
47
  }
47
48
 
48
49
  export async function OnSale({ section }: { section: ShelfSection }) {
49
50
  const { data } = await api.productsIndex({
50
51
  query: { 'filter[onSale]': 'true', perPage: section.settings.limit ?? SHELF_SIZE },
51
52
  });
52
- return <Shelf title="On sale" products={data} />;
53
+ return <Shelf title={messages(locale).onSale} products={data} />;
53
54
  }
54
55
 
55
56
  export async function Trending({ section }: { section: ShelfSection }) {
56
57
  const { data } = await api.productsIndex({
57
58
  query: { 'filter[trending]': 'true', perPage: section.settings.limit ?? SHELF_SIZE },
58
59
  });
59
- return <Shelf title="Trending" products={data} />;
60
+ return <Shelf title={messages(locale).trending} products={data} />;
60
61
  }
61
62
 
62
63
  export async function Bestsellers({ section }: { section: ShelfSection }) {
63
64
  const { data } = await api.productsIndex({
64
65
  query: { sort: '-bestSellers', perPage: section.settings.limit ?? SHELF_SIZE },
65
66
  });
66
- return <Shelf title="Bestsellers" products={data} />;
67
+ return <Shelf title={messages(locale).bestsellers} products={data} />;
67
68
  }
68
69
 
69
70
  export async function PreOrders({ section }: { section: ShelfSection }) {
70
71
  const { data } = await api.productsIndex({
71
72
  query: { 'filter[preOrder]': 'true', perPage: section.settings.limit ?? SHELF_SIZE },
72
73
  });
73
- return <Shelf title="Pre-order" products={data} />;
74
+ return <Shelf title={messages(locale).preOrder} products={data} />;
74
75
  }
75
76
 
76
77
  /**
@@ -1,6 +1,7 @@
1
1
  import Link from 'next/link';
2
2
 
3
- import { api } from '@/lib/api';
3
+ import { api, locale } from '@/lib/api';
4
+ import { messages } from '@/lib/i18n';
4
5
 
5
6
  import type { SectionOf } from './types';
6
7
 
@@ -8,6 +9,7 @@ const SOURCE_LABELS = { GOOGLE: 'Google', YANDEX: 'Yandex', TWO_GIS: '2GIS', CUS
8
9
 
9
10
  /** Reviews of the shop itself (`GET /reviews/site`), dressed with what the merchant entered. */
10
11
  export async function StoreReviews({ section }: { section: SectionOf<'STORE_REVIEWS'> }) {
12
+ const t = messages(locale);
11
13
  const settings = section.settings;
12
14
  const { data: reviews, meta } = await api.reviewsSite({
13
15
  query: { perPage: settings.limit, sort: '-createdAt' },
@@ -17,17 +19,17 @@ export async function StoreReviews({ section }: { section: SectionOf<'STORE_REVI
17
19
  return (
18
20
  <section className="stack">
19
21
  {settings.overline && <span className="muted">{settings.overline}</span>}
20
- <h2>{settings.title ?? 'What our customers say'}</h2>
22
+ <h2>{settings.title ?? t.customersSay}</h2>
21
23
  {settings.subtitle && <p className="muted">{settings.subtitle}</p>}
22
24
 
23
25
  <div className="row">
24
26
  {rating.average !== null && (
25
27
  <strong>
26
- {rating.average} / 5 · {rating.count} reviews
28
+ {rating.average} / 5 · {t.reviewsCount(rating.count)}
27
29
  </strong>
28
30
  )}
29
31
  {settings.recommendPercent !== null && (
30
- <span>{settings.recommendPercent}% recommend us</span>
32
+ <span>{t.recommendUs(settings.recommendPercent)}</span>
31
33
  )}
32
34
  {settings.externalRatings.map((external, index) => (
33
35
  <span key={index} className="muted">
@@ -59,7 +61,7 @@ export async function StoreReviews({ section }: { section: SectionOf<'STORE_REVI
59
61
  {settings.showLeaveReview && (
60
62
  // Leaving a site review needs a signed-in customer (POST /reviews/site).
61
63
  <Link href="/account" className="button">
62
- Leave a review
64
+ {t.leaveReview}
63
65
  </Link>
64
66
  )}
65
67
  </section>
@@ -1,9 +1,6 @@
1
1
  import { MagicStoreError } from '@magicstoreai/hydrogen';
2
2
 
3
3
  /** What to tell the buyer: the API's localized detail when there is one. */
4
- export function describe(
5
- error: unknown,
6
- fallback = 'Something went wrong. Please try again.',
7
- ): string {
4
+ export function describe(error: unknown, fallback: string): string {
8
5
  return error instanceof MagicStoreError ? (error.detail ?? error.message) : fallback;
9
6
  }