create-brainerce-store 1.62.0 → 1.64.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 (203) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +71 -71
  3. package/dist/index.js +26 -3
  4. package/messages/en.json +502 -502
  5. package/messages/he.json +502 -502
  6. package/package.json +1 -1
  7. package/templates/nextjs/base/.claude/commands/design.md +60 -60
  8. package/templates/nextjs/base/.env.local.ejs +24 -24
  9. package/templates/nextjs/base/.eslintrc.json +57 -57
  10. package/templates/nextjs/base/.mcp.json +8 -8
  11. package/templates/nextjs/base/AI-GUIDE.md +174 -174
  12. package/templates/nextjs/base/TRANSLATIONS.md +200 -200
  13. package/templates/nextjs/base/package.json.ejs +52 -52
  14. package/templates/nextjs/base/src/app/agents.md/route.ts +87 -0
  15. package/templates/nextjs/base/src/app/blog/[slug]/page.tsx.ejs +308 -262
  16. package/templates/nextjs/base/src/app/blog/page.tsx.ejs +276 -276
  17. package/templates/nextjs/base/src/app/blog/rss.xml/route.ts +70 -0
  18. package/templates/nextjs/base/src/app/cart/page.tsx +7 -7
  19. package/templates/nextjs/base/src/app/category/[slug]/page.tsx +29 -3
  20. package/templates/nextjs/base/src/app/checkout/page.tsx +12 -0
  21. package/templates/nextjs/base/src/app/contact/layout.tsx.ejs +14 -0
  22. package/templates/nextjs/base/src/app/globals.css +36 -36
  23. package/templates/nextjs/base/src/app/indexnow-key.txt/route.ts +26 -26
  24. package/templates/nextjs/base/src/app/layout.tsx.ejs +30 -0
  25. package/templates/nextjs/base/src/app/llms.txt/route.ts +28 -5
  26. package/templates/nextjs/base/src/app/not-found.tsx.ejs +35 -0
  27. package/templates/nextjs/base/src/app/opengraph-image.tsx +109 -0
  28. package/templates/nextjs/base/src/app/order-confirmation/page.tsx +295 -278
  29. package/templates/nextjs/base/src/app/order-status/page.tsx +121 -121
  30. package/templates/nextjs/base/src/app/page.tsx +64 -64
  31. package/templates/nextjs/base/src/app/products/[slug]/page.tsx +14 -2
  32. package/templates/nextjs/base/src/app/products/page.tsx +19 -19
  33. package/templates/nextjs/base/src/app/robots.ts +73 -14
  34. package/templates/nextjs/base/src/app/sitemap.ts +89 -66
  35. package/templates/nextjs/base/src/components/account/order-history.tsx +385 -385
  36. package/templates/nextjs/base/src/components/brainerce-bot.tsx +70 -70
  37. package/templates/nextjs/base/src/components/checkout/checkout-form.tsx +612 -612
  38. package/templates/nextjs/base/src/components/checkout/custom-fields-step.tsx +292 -292
  39. package/templates/nextjs/base/src/components/checkout/date-picker.tsx +309 -309
  40. package/templates/nextjs/base/src/components/checkout/datetime-picker.tsx +144 -144
  41. package/templates/nextjs/base/src/components/checkout/order-bump-card.tsx +242 -242
  42. package/templates/nextjs/base/src/components/checkout/pickup-step.tsx +198 -198
  43. package/templates/nextjs/base/src/components/checkout/shipping-step.tsx +125 -125
  44. package/templates/nextjs/base/src/components/checkout/tax-display.tsx +74 -74
  45. package/templates/nextjs/base/src/components/seo/article-json-ld.tsx +59 -59
  46. package/templates/nextjs/base/src/components/seo/breadcrumbs.tsx +37 -37
  47. package/templates/nextjs/base/src/components/seo/category-json-ld.tsx +61 -61
  48. package/templates/nextjs/base/src/components/seo/organization-json-ld.tsx +97 -97
  49. package/templates/nextjs/base/src/components/seo/product-json-ld.tsx +23 -131
  50. package/templates/nextjs/base/src/components/tracking-bootstrap.tsx +62 -0
  51. package/templates/nextjs/base/src/components/ui/accordion.tsx +53 -53
  52. package/templates/nextjs/base/src/components/ui/badge.tsx +32 -32
  53. package/templates/nextjs/base/src/components/ui/button.tsx +49 -49
  54. package/templates/nextjs/base/src/components/ui/card.tsx +55 -55
  55. package/templates/nextjs/base/src/components/ui/checkbox.tsx +28 -28
  56. package/templates/nextjs/base/src/components/ui/dialog.tsx +107 -107
  57. package/templates/nextjs/base/src/components/ui/input.tsx +22 -22
  58. package/templates/nextjs/base/src/components/ui/label.tsx +21 -21
  59. package/templates/nextjs/base/src/components/ui/radio-group.tsx +38 -38
  60. package/templates/nextjs/base/src/components/ui/select.tsx +153 -153
  61. package/templates/nextjs/base/src/components/ui/separator.tsx +26 -26
  62. package/templates/nextjs/base/src/components/ui/sheet.tsx +125 -125
  63. package/templates/nextjs/base/src/components/ui/skeleton.tsx +7 -7
  64. package/templates/nextjs/base/src/components/ui/tabs.tsx +55 -55
  65. package/templates/nextjs/base/src/components/ui/textarea.tsx +21 -21
  66. package/templates/nextjs/base/src/components/ui/tooltip.tsx +32 -32
  67. package/templates/nextjs/base/src/core/hooks/use-cart-page.ts +58 -58
  68. package/templates/nextjs/base/src/core/hooks/use-home-data.ts +51 -51
  69. package/templates/nextjs/base/src/core/hooks/use-product-listing.ts +295 -295
  70. package/templates/nextjs/base/src/core/hooks/use-product-page.ts +20 -2
  71. package/templates/nextjs/base/src/core/lib/image-hosts.ts +7 -7
  72. package/templates/nextjs/base/src/core/lib/product-options.ts +121 -121
  73. package/templates/nextjs/base/src/core/lib/resolve-currency.ts +20 -20
  74. package/templates/nextjs/base/src/core/lib/safe-redirect.ts +38 -38
  75. package/templates/nextjs/base/src/core/lib/seo.ts +46 -46
  76. package/templates/nextjs/base/src/core/lib/store-info.ts +17 -0
  77. package/templates/nextjs/base/src/core/lib/tracking.ts +171 -0
  78. package/templates/nextjs/base/src/core/lib/use-currency.ts +19 -19
  79. package/templates/nextjs/base/src/core/lib/utils.ts +21 -21
  80. package/templates/nextjs/base/src/middleware.ts.ejs +54 -2
  81. package/templates/nextjs/base/src/ui/cart/cart-bundle-offer.tsx +117 -117
  82. package/templates/nextjs/base/src/ui/cart/cart-item.tsx +146 -146
  83. package/templates/nextjs/base/src/ui/cart/cart-nudges.tsx +28 -28
  84. package/templates/nextjs/base/src/ui/cart/cart-summary.tsx +110 -110
  85. package/templates/nextjs/base/src/ui/cart/cart-upgrade-banner.tsx +126 -126
  86. package/templates/nextjs/base/src/ui/cart/cart-view.tsx +140 -140
  87. package/templates/nextjs/base/src/ui/cart/coupon-input.tsx +134 -134
  88. package/templates/nextjs/base/src/ui/cart/free-shipping-bar.tsx +61 -61
  89. package/templates/nextjs/base/src/ui/cart/reservation-countdown.tsx +95 -95
  90. package/templates/nextjs/base/src/ui/home/discount-banner-strip.tsx +22 -20
  91. package/templates/nextjs/base/src/ui/home/featured-products-section.tsx +24 -24
  92. package/templates/nextjs/base/src/ui/home/hero-section.tsx +27 -27
  93. package/templates/nextjs/base/src/ui/home/home-client.tsx +29 -29
  94. package/templates/nextjs/base/src/ui/layout/announcement-bar.tsx.ejs +116 -116
  95. package/templates/nextjs/base/src/ui/layout/faq-section.tsx.ejs +90 -90
  96. package/templates/nextjs/base/src/ui/layout/header-account.tsx +40 -40
  97. package/templates/nextjs/base/src/ui/layout/language-switcher.tsx.ejs +80 -80
  98. package/templates/nextjs/base/src/ui/layout/site-footer.tsx.ejs +144 -144
  99. package/templates/nextjs/base/src/ui/layout/site-header.tsx.ejs +152 -152
  100. package/templates/nextjs/base/src/ui/product/customization-fields.tsx +457 -457
  101. package/templates/nextjs/base/src/ui/product/discount-badge.tsx +18 -18
  102. package/templates/nextjs/base/src/ui/product/frequently-bought-together.tsx +191 -191
  103. package/templates/nextjs/base/src/ui/product/modifier-group-selector.tsx +206 -206
  104. package/templates/nextjs/base/src/ui/product/price-display.tsx +62 -62
  105. package/templates/nextjs/base/src/ui/product/product-card.tsx +7 -2
  106. package/templates/nextjs/base/src/ui/product/product-client-section.tsx +455 -455
  107. package/templates/nextjs/base/src/ui/product/product-listing.tsx +446 -446
  108. package/templates/nextjs/base/src/ui/product/recommendation-section.tsx +102 -102
  109. package/templates/nextjs/base/src/ui/product/review-form.tsx +294 -294
  110. package/templates/nextjs/base/src/ui/product/reviews-section.tsx.ejs +108 -108
  111. package/templates/nextjs/base/src/ui/product/stock-badge.tsx +61 -61
  112. package/templates/nextjs/base/src/ui/product/variant-selector.tsx +291 -291
  113. package/templates/nextjs/base/src/ui/shared/cdn-image.tsx +26 -26
  114. package/templates/nextjs/base/tailwind.config.ts +69 -69
  115. package/templates/nextjs/designs/atelier/app-overlay/layout.tsx.ejs +30 -0
  116. package/templates/nextjs/designs/atelier/globals.css +402 -402
  117. package/templates/nextjs/designs/atelier/messages-patch/en.json +100 -100
  118. package/templates/nextjs/designs/atelier/messages-patch/he.json +100 -100
  119. package/templates/nextjs/designs/atelier/ui/cart/cart-bundle-offer.tsx +110 -110
  120. package/templates/nextjs/designs/atelier/ui/cart/cart-drawer.tsx +163 -163
  121. package/templates/nextjs/designs/atelier/ui/cart/cart-item.tsx +140 -140
  122. package/templates/nextjs/designs/atelier/ui/cart/cart-nudges.tsx +34 -34
  123. package/templates/nextjs/designs/atelier/ui/cart/cart-summary.tsx +107 -107
  124. package/templates/nextjs/designs/atelier/ui/cart/cart-upgrade-banner.tsx +123 -123
  125. package/templates/nextjs/designs/atelier/ui/cart/cart-view.tsx +147 -147
  126. package/templates/nextjs/designs/atelier/ui/cart/coupon-input.tsx +142 -142
  127. package/templates/nextjs/designs/atelier/ui/cart/free-shipping-bar.tsx +77 -77
  128. package/templates/nextjs/designs/atelier/ui/cart/reservation-countdown.tsx +89 -89
  129. package/templates/nextjs/designs/atelier/ui/home/benefits-band.tsx +34 -34
  130. package/templates/nextjs/designs/atelier/ui/home/category-tiles.tsx +151 -151
  131. package/templates/nextjs/designs/atelier/ui/home/discount-banner-strip.tsx +27 -27
  132. package/templates/nextjs/designs/atelier/ui/home/editorial-band.tsx +75 -75
  133. package/templates/nextjs/designs/atelier/ui/home/featured-products-section.tsx +36 -36
  134. package/templates/nextjs/designs/atelier/ui/home/hero-section.tsx +174 -174
  135. package/templates/nextjs/designs/atelier/ui/home/home-client.tsx +54 -54
  136. package/templates/nextjs/designs/atelier/ui/home/newsletter-section.tsx +78 -78
  137. package/templates/nextjs/designs/atelier/ui/home/testimonials-band.tsx +46 -46
  138. package/templates/nextjs/designs/atelier/ui/layout/announcement-bar.tsx +118 -118
  139. package/templates/nextjs/designs/atelier/ui/layout/faq-section.tsx +96 -96
  140. package/templates/nextjs/designs/atelier/ui/layout/header-account.tsx +36 -36
  141. package/templates/nextjs/designs/atelier/ui/layout/header-cart.tsx +36 -36
  142. package/templates/nextjs/designs/atelier/ui/layout/language-switcher.tsx.ejs +137 -137
  143. package/templates/nextjs/designs/atelier/ui/layout/nav-url.ts +14 -14
  144. package/templates/nextjs/designs/atelier/ui/layout/rich-text-block.tsx +34 -34
  145. package/templates/nextjs/designs/atelier/ui/layout/site-footer.tsx.ejs +141 -141
  146. package/templates/nextjs/designs/atelier/ui/layout/site-header.tsx.ejs +138 -138
  147. package/templates/nextjs/designs/atelier/ui/product/customization-fields.tsx +466 -466
  148. package/templates/nextjs/designs/atelier/ui/product/discount-badge.tsx +17 -17
  149. package/templates/nextjs/designs/atelier/ui/product/frequently-bought-together.tsx +196 -196
  150. package/templates/nextjs/designs/atelier/ui/product/modifier-group-selector.tsx +193 -193
  151. package/templates/nextjs/designs/atelier/ui/product/price-display.tsx +69 -69
  152. package/templates/nextjs/designs/atelier/ui/product/product-card.tsx +13 -11
  153. package/templates/nextjs/designs/atelier/ui/product/product-client-section.tsx +477 -477
  154. package/templates/nextjs/designs/atelier/ui/product/product-grid.tsx +42 -42
  155. package/templates/nextjs/designs/atelier/ui/product/product-listing.tsx +260 -260
  156. package/templates/nextjs/designs/atelier/ui/product/recommendation-section.tsx +95 -95
  157. package/templates/nextjs/designs/atelier/ui/product/review-form.tsx +267 -267
  158. package/templates/nextjs/designs/atelier/ui/product/reviews-section.tsx.ejs +148 -148
  159. package/templates/nextjs/designs/atelier/ui/product/stock-badge.tsx +66 -66
  160. package/templates/nextjs/designs/atelier/ui/product/variant-selector.tsx +206 -206
  161. package/templates/nextjs/designs/atelier/ui/shared/cdn-image.tsx +29 -29
  162. package/templates/nextjs/designs/atelier/ui/shared/fly-to-cart.ts +86 -86
  163. package/templates/nextjs/designs/atelier/ui/shared/icons.tsx +276 -276
  164. package/templates/nextjs/designs/atelier/ui/shared/loading-spinner.tsx +63 -63
  165. package/templates/nextjs/designs/atelier/ui/shared/reveal.tsx +71 -71
  166. package/templates/nextjs/designs/atelier/ui/shared/select-menu.tsx +125 -125
  167. package/templates/nextjs/ui-canvas/cart/cart-bundle-offer.tsx +89 -89
  168. package/templates/nextjs/ui-canvas/cart/cart-item.tsx +123 -123
  169. package/templates/nextjs/ui-canvas/cart/cart-nudges.tsx +29 -27
  170. package/templates/nextjs/ui-canvas/cart/cart-summary.tsx +99 -99
  171. package/templates/nextjs/ui-canvas/cart/cart-upgrade-banner.tsx +105 -105
  172. package/templates/nextjs/ui-canvas/cart/cart-view.tsx +106 -106
  173. package/templates/nextjs/ui-canvas/cart/coupon-input.tsx +119 -119
  174. package/templates/nextjs/ui-canvas/cart/free-shipping-bar.tsx +54 -54
  175. package/templates/nextjs/ui-canvas/cart/reservation-countdown.tsx +81 -81
  176. package/templates/nextjs/ui-canvas/home/discount-banner-strip.tsx +21 -19
  177. package/templates/nextjs/ui-canvas/home/featured-products-section.tsx +23 -23
  178. package/templates/nextjs/ui-canvas/home/hero-section.tsx +21 -21
  179. package/templates/nextjs/ui-canvas/home/home-client.tsx +26 -26
  180. package/templates/nextjs/ui-canvas/layout/announcement-bar.tsx.ejs +101 -101
  181. package/templates/nextjs/ui-canvas/layout/faq-section.tsx.ejs +71 -71
  182. package/templates/nextjs/ui-canvas/layout/header-account.tsx +31 -31
  183. package/templates/nextjs/ui-canvas/layout/language-switcher.tsx.ejs +76 -76
  184. package/templates/nextjs/ui-canvas/layout/rich-text-block.tsx.ejs +36 -36
  185. package/templates/nextjs/ui-canvas/layout/site-footer.tsx.ejs +82 -82
  186. package/templates/nextjs/ui-canvas/layout/site-header.tsx.ejs +119 -119
  187. package/templates/nextjs/ui-canvas/product/customization-fields.tsx +445 -445
  188. package/templates/nextjs/ui-canvas/product/discount-badge.tsx +19 -19
  189. package/templates/nextjs/ui-canvas/product/frequently-bought-together.tsx +168 -168
  190. package/templates/nextjs/ui-canvas/product/modifier-group-selector.tsx +165 -165
  191. package/templates/nextjs/ui-canvas/product/price-display.tsx +52 -52
  192. package/templates/nextjs/ui-canvas/product/product-card.tsx +7 -2
  193. package/templates/nextjs/ui-canvas/product/product-client-section.tsx +352 -352
  194. package/templates/nextjs/ui-canvas/product/product-grid.tsx +27 -27
  195. package/templates/nextjs/ui-canvas/product/product-listing.tsx +224 -224
  196. package/templates/nextjs/ui-canvas/product/recommendation-section.tsx +99 -99
  197. package/templates/nextjs/ui-canvas/product/review-form.tsx +266 -266
  198. package/templates/nextjs/ui-canvas/product/reviews-section.tsx.ejs +96 -96
  199. package/templates/nextjs/ui-canvas/product/stock-badge.tsx +45 -45
  200. package/templates/nextjs/ui-canvas/product/variant-selector.tsx +212 -212
  201. package/templates/nextjs/ui-canvas/shared/cdn-image.tsx +31 -31
  202. package/templates/nextjs/ui-canvas/shared/loading-spinner.tsx +27 -27
  203. package/templates/nextjs/base/tsconfig.tsbuildinfo +0 -1
@@ -1,200 +1,200 @@
1
- # Multi-language storefronts
2
-
3
- Your scaffolded Brainerce store is already wired for multi-language out of the box. This doc explains the moving parts so you can customize them — you do **not** need to write per-locale fetch code; the SDK + middleware do it for you.
4
-
5
- ## Status check
6
-
7
- ```typescript
8
- const store = await client.getStoreInfo();
9
- store.i18n?.enabled; // → true / false
10
- store.i18n?.defaultLocale; // → e.g. "en"
11
- store.i18n?.supportedLocales; // → e.g. ["en", "he"]
12
- ```
13
-
14
- If `i18n.enabled` is `false` or only one locale is supported, the rest of this doc is a no-op — the app behaves as a single-language store.
15
-
16
- ## URL strategy: "as-needed" locale prefix
17
-
18
- This template uses the as-needed pattern (the most common approach for SEO):
19
-
20
- | URL | Locale | Notes |
21
- | -------------- | ------- | ------------------------------------------------- |
22
- | `/` | default | Clean URL — no `/en` prefix on the default locale |
23
- | `/products` | default | Same |
24
- | `/he` | Hebrew | Secondary locales get a path prefix |
25
- | `/he/products` | Hebrew | Same |
26
-
27
- The middleware (`src/middleware.ts`) handles two transitions:
28
-
29
- 1. `/{defaultLocale}/X` → 308 redirect to `/X` (canonicalize away the redundant prefix)
30
- 2. `/X` → internal rewrite to `/{defaultLocale}/X` so the Next.js `[locale]` route segment still resolves
31
-
32
- Every response carries an `x-locale` header so Server Components can read the resolved locale via `headers()`.
33
-
34
- ## How translated content shows up on the page
35
-
36
- You write **one** `fetch` and it works for every language.
37
-
38
- ```tsx
39
- // src/app/[locale]/products/[slug]/page.tsx
40
- import { getServerClient } from '@/core/lib/server-client';
41
- import { headers } from 'next/headers';
42
-
43
- export default async function ProductPage({ params }: { params: Promise<{ slug: string }> }) {
44
- const { slug } = await params;
45
- const locale = (await headers()).get('x-locale') ?? undefined;
46
- const client = getServerClient();
47
- client.setLocale(locale);
48
-
49
- const product = await client.getProductBySlug(slug);
50
- // product.name, product.description, product.categories[].name, modifier groups,
51
- // metafield labels — all already translated by the server.
52
-
53
- return <ProductDetail product={product} />;
54
- }
55
- ```
56
-
57
- The `StoreProvider` (`src/providers/store-provider.tsx`) calls `client.setLocale(locale)` on the client side, so React Server Components and Client Components both get translated content.
58
-
59
- ## What's translatable (full list)
60
-
61
- | Entity | Fields |
62
- | ----------------------- | ----------------------------------------------------------- |
63
- | **Product** | `name`, `description`, `slug`, `seoTitle`, `seoDescription` |
64
- | **ProductVariant** | `name` |
65
- | **Category** | `name` |
66
- | **Brand** | `name` |
67
- | **Tag** | `name` |
68
- | **Attribute** | `name` (e.g. "Color") |
69
- | **AttributeOption** | `name` (e.g. "Red") |
70
- | **ModifierGroup** | `name`, `description` (e.g. "Toppings" → "תוספות") |
71
- | **Modifier** | `name`, `description` (e.g. "Olives" → "זיתים") |
72
- | **ProductMetafield** | `value` (free-text custom field values) |
73
- | **MetafieldDefinition** | `name`, `description` (custom-field labels) |
74
- | **BundleOffer** | `name`, `description` (bundle marketing label) |
75
- | **OrderBumpConfig** | `title`, `description` (bump headline at checkout) |
76
- | **DiscountRule** | `name`, `description` (rule label, used in banners) |
77
- | **ContactForm** | `name`, `description`, `submitButton`, `successMessage` |
78
- | **ContactFormField** | `label`, `placeholder`, `helpText` |
79
-
80
- You never overlay translations yourself — the SDK does it on every request.
81
-
82
- ## RTL (Hebrew, Arabic, Persian, Urdu, Yiddish)
83
-
84
- `src/i18n.ts` exports `getDirection(locale)` that delegates to the SDK's `getDirectionForLocale()`. The layout uses it on `<html dir={…}>`:
85
-
86
- ```tsx
87
- // src/app/[locale]/layout.tsx
88
- import { getDirection } from '@/i18n';
89
-
90
- export default async function LocaleLayout({ children, params }: Props) {
91
- const { locale } = await params;
92
- const dir = getDirection(locale);
93
- return (
94
- <html lang={locale} dir={dir}>
95
- <body>{children}</body>
96
- </html>
97
- );
98
- }
99
- ```
100
-
101
- This automatically reverses flexbox row order — **do not add `flex-row-reverse`** on top, that's a double-swap. **Do** swap directional icons (chevrons, arrows) using `useDirection()` from `@radix-ui/react-direction`.
102
-
103
- Use logical Tailwind classes (`ms-*`/`me-*` for margin, `ps-*`/`pe-*` for padding, `start-*`/`end-*` for positioning) instead of physical ones (`ml-*`, `mr-*`, `left-*`, `right-*`) so the layout mirrors automatically.
104
-
105
- ## Language switcher
106
-
107
- ```tsx
108
- 'use client';
109
- import { useStore } from '@/providers/store-provider';
110
- import Link from 'next/link';
111
- import { useParams, usePathname } from 'next/navigation';
112
-
113
- export function LanguageSwitcher() {
114
- const { storeInfo } = useStore();
115
- const pathname = usePathname();
116
- const { locale: current } = useParams<{ locale?: string }>();
117
-
118
- if (!storeInfo?.i18n?.enabled) return null;
119
- const locales = storeInfo.i18n.supportedLocales;
120
- const defaultLocale = storeInfo.i18n.defaultLocale;
121
-
122
- return (
123
- <nav className="flex gap-2">
124
- {locales.map((loc) => {
125
- const isCurrent = (current ?? defaultLocale) === loc;
126
- const href = loc === defaultLocale ? pathname : `/${loc}${pathname}`;
127
- return (
128
- <Link key={loc} href={href} className={isCurrent ? 'font-bold' : ''}>
129
- {loc.toUpperCase()}
130
- </Link>
131
- );
132
- })}
133
- </nav>
134
- );
135
- }
136
- ```
137
-
138
- The merchant configures supported locales in `Dashboard → Settings → Languages`. Your switcher reads them from `storeInfo.i18n.supportedLocales` — never hardcode a list.
139
-
140
- ## SEO: per-locale slugs and hreflang
141
-
142
- When the merchant translates a product's `slug`, every locale gets its own URL (e.g. `/cheese-pizza` and `/he/פיצה-גבינה`). Pull all alternates in one call for the `<head>`:
143
-
144
- ```tsx
145
- const alternates = await client.getProductAlternates(product.id);
146
- // → [{ locale: 'en', slug: 'cheese-pizza' }, { locale: 'he', slug: 'פיצה-גבינה' }]
147
-
148
- // In generateMetadata:
149
- return {
150
- alternates: {
151
- languages: Object.fromEntries(
152
- alternates.map((a) => [a.locale, `/${a.locale}/products/${a.slug}`])
153
- ),
154
- },
155
- };
156
- ```
157
-
158
- ## Promotional surfaces: bundles, bumps, discount banners
159
-
160
- The same overlay applies to every promotional surface — you don't need locale-aware code:
161
-
162
- ```tsx
163
- // Cart bundles (cross-sell) — locale-aware automatically:
164
- const cart = await client.getCart(cartId);
165
- cart.bundles[0].name; // "ארוחת צהריים" (bundle's own label)
166
- cart.bundles[0].offeredProducts[0].name; // "פיצה גבינה" (each offered product)
167
-
168
- // Order bumps at checkout:
169
- const { bumps } = await client.getOrderBumps(checkoutId);
170
- bumps[0].title; // translated bump headline (or merchant override)
171
- bumps[0].bumpProduct.name; // translated product name
172
-
173
- // Discount-rule banners (rendered from rule.name + rule.displayConfig):
174
- const rules = await client.getActiveDiscountRules();
175
- rules[0].name; // translated rule name
176
- ```
177
-
178
- ## How merchants populate translations
179
-
180
- For background — your storefront doesn't need to call these endpoints, but knowing the merchant flow helps when debugging unexpectedly-empty translations:
181
-
182
- - **Per-row overlay** on every taxonomy/product list page in the dashboard.
183
- - **Inside the entity create/edit modal** — a `LocaleSelector` in the header + "Translate with AI" button populates target-locale fields (Products, Attributes, Modifier Groups, Modifiers, Custom Fields).
184
- - **Translate icon button** on bundle / order-bump rows (`/products/.../offers`) and on discount-rule rows (`/discount-rules`) opens a standalone translation modal with one-click AI.
185
- - **Bulk** — select N rows on a list page → toolbar action "Translate to Hebrew" enqueues an AI translation job for everything selected (including children, e.g. all modifiers under selected groups).
186
-
187
- Translations are persisted via the dashboard-only endpoint `PUT /api/stores/:storeId/translations/:entityType/:entityId/:locale`. The storefront SDK never calls this — it only consumes the overlay on read.
188
-
189
- ## Troubleshooting
190
-
191
- | Symptom | Likely cause | Fix |
192
- | -------------------------------------------------------------- | --------------------------------------------------- | --------------------------------------------------------------------------------- |
193
- | `product.name` is English even though locale is `he` | Store doesn't have `he` in `supportedLocales` | Add the locale in `Dashboard → Settings → Languages` |
194
- | Some fields translate, others don't | Merchant translated subset | Per-field fallback is by design — fill the rest in dashboard |
195
- | Layout broken in Hebrew | Missing `<html dir="rtl">` | Use `getDirection(locale)` in the layout |
196
- | Modifier names in default language but product name translates | Merchant translated `Product` only | Bulk translate on `/products/modifier-groups` covers groups + all their modifiers |
197
- | Custom-field label "Warranty" doesn't translate | Merchant didn't translate the `MetafieldDefinition` | Per-row "Translate" on `/products/custom-fields` |
198
- | Bundle name "Summer Sale" stays in English in Hebrew cart | Merchant didn't translate the `BundleOffer` itself | Click the Languages icon on the bundle row in `/products/.../offers` |
199
-
200
- See [the Brainerce docs](https://brainerce.com/docs/concepts/translations) for the canonical reference.
1
+ # Multi-language storefronts
2
+
3
+ Your scaffolded Brainerce store is already wired for multi-language out of the box. This doc explains the moving parts so you can customize them — you do **not** need to write per-locale fetch code; the SDK + middleware do it for you.
4
+
5
+ ## Status check
6
+
7
+ ```typescript
8
+ const store = await client.getStoreInfo();
9
+ store.i18n?.enabled; // → true / false
10
+ store.i18n?.defaultLocale; // → e.g. "en"
11
+ store.i18n?.supportedLocales; // → e.g. ["en", "he"]
12
+ ```
13
+
14
+ If `i18n.enabled` is `false` or only one locale is supported, the rest of this doc is a no-op — the app behaves as a single-language store.
15
+
16
+ ## URL strategy: "as-needed" locale prefix
17
+
18
+ This template uses the as-needed pattern (the most common approach for SEO):
19
+
20
+ | URL | Locale | Notes |
21
+ | -------------- | ------- | ------------------------------------------------- |
22
+ | `/` | default | Clean URL — no `/en` prefix on the default locale |
23
+ | `/products` | default | Same |
24
+ | `/he` | Hebrew | Secondary locales get a path prefix |
25
+ | `/he/products` | Hebrew | Same |
26
+
27
+ The middleware (`src/middleware.ts`) handles two transitions:
28
+
29
+ 1. `/{defaultLocale}/X` → 308 redirect to `/X` (canonicalize away the redundant prefix)
30
+ 2. `/X` → internal rewrite to `/{defaultLocale}/X` so the Next.js `[locale]` route segment still resolves
31
+
32
+ Every response carries an `x-locale` header so Server Components can read the resolved locale via `headers()`.
33
+
34
+ ## How translated content shows up on the page
35
+
36
+ You write **one** `fetch` and it works for every language.
37
+
38
+ ```tsx
39
+ // src/app/[locale]/products/[slug]/page.tsx
40
+ import { getServerClient } from '@/core/lib/server-client';
41
+ import { headers } from 'next/headers';
42
+
43
+ export default async function ProductPage({ params }: { params: Promise<{ slug: string }> }) {
44
+ const { slug } = await params;
45
+ const locale = (await headers()).get('x-locale') ?? undefined;
46
+ const client = getServerClient();
47
+ client.setLocale(locale);
48
+
49
+ const product = await client.getProductBySlug(slug);
50
+ // product.name, product.description, product.categories[].name, modifier groups,
51
+ // metafield labels — all already translated by the server.
52
+
53
+ return <ProductDetail product={product} />;
54
+ }
55
+ ```
56
+
57
+ The `StoreProvider` (`src/providers/store-provider.tsx`) calls `client.setLocale(locale)` on the client side, so React Server Components and Client Components both get translated content.
58
+
59
+ ## What's translatable (full list)
60
+
61
+ | Entity | Fields |
62
+ | ----------------------- | ----------------------------------------------------------- |
63
+ | **Product** | `name`, `description`, `slug`, `seoTitle`, `seoDescription` |
64
+ | **ProductVariant** | `name` |
65
+ | **Category** | `name` |
66
+ | **Brand** | `name` |
67
+ | **Tag** | `name` |
68
+ | **Attribute** | `name` (e.g. "Color") |
69
+ | **AttributeOption** | `name` (e.g. "Red") |
70
+ | **ModifierGroup** | `name`, `description` (e.g. "Toppings" → "תוספות") |
71
+ | **Modifier** | `name`, `description` (e.g. "Olives" → "זיתים") |
72
+ | **ProductMetafield** | `value` (free-text custom field values) |
73
+ | **MetafieldDefinition** | `name`, `description` (custom-field labels) |
74
+ | **BundleOffer** | `name`, `description` (bundle marketing label) |
75
+ | **OrderBumpConfig** | `title`, `description` (bump headline at checkout) |
76
+ | **DiscountRule** | `name`, `description` (rule label, used in banners) |
77
+ | **ContactForm** | `name`, `description`, `submitButton`, `successMessage` |
78
+ | **ContactFormField** | `label`, `placeholder`, `helpText` |
79
+
80
+ You never overlay translations yourself — the SDK does it on every request.
81
+
82
+ ## RTL (Hebrew, Arabic, Persian, Urdu, Yiddish)
83
+
84
+ `src/i18n.ts` exports `getDirection(locale)` that delegates to the SDK's `getDirectionForLocale()`. The layout uses it on `<html dir={…}>`:
85
+
86
+ ```tsx
87
+ // src/app/[locale]/layout.tsx
88
+ import { getDirection } from '@/i18n';
89
+
90
+ export default async function LocaleLayout({ children, params }: Props) {
91
+ const { locale } = await params;
92
+ const dir = getDirection(locale);
93
+ return (
94
+ <html lang={locale} dir={dir}>
95
+ <body>{children}</body>
96
+ </html>
97
+ );
98
+ }
99
+ ```
100
+
101
+ This automatically reverses flexbox row order — **do not add `flex-row-reverse`** on top, that's a double-swap. **Do** swap directional icons (chevrons, arrows) using `useDirection()` from `@radix-ui/react-direction`.
102
+
103
+ Use logical Tailwind classes (`ms-*`/`me-*` for margin, `ps-*`/`pe-*` for padding, `start-*`/`end-*` for positioning) instead of physical ones (`ml-*`, `mr-*`, `left-*`, `right-*`) so the layout mirrors automatically.
104
+
105
+ ## Language switcher
106
+
107
+ ```tsx
108
+ 'use client';
109
+ import { useStore } from '@/providers/store-provider';
110
+ import Link from 'next/link';
111
+ import { useParams, usePathname } from 'next/navigation';
112
+
113
+ export function LanguageSwitcher() {
114
+ const { storeInfo } = useStore();
115
+ const pathname = usePathname();
116
+ const { locale: current } = useParams<{ locale?: string }>();
117
+
118
+ if (!storeInfo?.i18n?.enabled) return null;
119
+ const locales = storeInfo.i18n.supportedLocales;
120
+ const defaultLocale = storeInfo.i18n.defaultLocale;
121
+
122
+ return (
123
+ <nav className="flex gap-2">
124
+ {locales.map((loc) => {
125
+ const isCurrent = (current ?? defaultLocale) === loc;
126
+ const href = loc === defaultLocale ? pathname : `/${loc}${pathname}`;
127
+ return (
128
+ <Link key={loc} href={href} className={isCurrent ? 'font-bold' : ''}>
129
+ {loc.toUpperCase()}
130
+ </Link>
131
+ );
132
+ })}
133
+ </nav>
134
+ );
135
+ }
136
+ ```
137
+
138
+ The merchant configures supported locales in `Dashboard → Settings → Languages`. Your switcher reads them from `storeInfo.i18n.supportedLocales` — never hardcode a list.
139
+
140
+ ## SEO: per-locale slugs and hreflang
141
+
142
+ When the merchant translates a product's `slug`, every locale gets its own URL (e.g. `/cheese-pizza` and `/he/פיצה-גבינה`). Pull all alternates in one call for the `<head>`:
143
+
144
+ ```tsx
145
+ const alternates = await client.getProductAlternates(product.id);
146
+ // → [{ locale: 'en', slug: 'cheese-pizza' }, { locale: 'he', slug: 'פיצה-גבינה' }]
147
+
148
+ // In generateMetadata:
149
+ return {
150
+ alternates: {
151
+ languages: Object.fromEntries(
152
+ alternates.map((a) => [a.locale, `/${a.locale}/products/${a.slug}`])
153
+ ),
154
+ },
155
+ };
156
+ ```
157
+
158
+ ## Promotional surfaces: bundles, bumps, discount banners
159
+
160
+ The same overlay applies to every promotional surface — you don't need locale-aware code:
161
+
162
+ ```tsx
163
+ // Cart bundles (cross-sell) — locale-aware automatically:
164
+ const cart = await client.getCart(cartId);
165
+ cart.bundles[0].name; // "ארוחת צהריים" (bundle's own label)
166
+ cart.bundles[0].offeredProducts[0].name; // "פיצה גבינה" (each offered product)
167
+
168
+ // Order bumps at checkout:
169
+ const { bumps } = await client.getOrderBumps(checkoutId);
170
+ bumps[0].title; // translated bump headline (or merchant override)
171
+ bumps[0].bumpProduct.name; // translated product name
172
+
173
+ // Discount-rule banners (rendered from rule.name + rule.displayConfig):
174
+ const rules = await client.getActiveDiscountRules();
175
+ rules[0].name; // translated rule name
176
+ ```
177
+
178
+ ## How merchants populate translations
179
+
180
+ For background — your storefront doesn't need to call these endpoints, but knowing the merchant flow helps when debugging unexpectedly-empty translations:
181
+
182
+ - **Per-row overlay** on every taxonomy/product list page in the dashboard.
183
+ - **Inside the entity create/edit modal** — a `LocaleSelector` in the header + "Translate with AI" button populates target-locale fields (Products, Attributes, Modifier Groups, Modifiers, Custom Fields).
184
+ - **Translate icon button** on bundle / order-bump rows (`/products/.../offers`) and on discount-rule rows (`/discount-rules`) opens a standalone translation modal with one-click AI.
185
+ - **Bulk** — select N rows on a list page → toolbar action "Translate to Hebrew" enqueues an AI translation job for everything selected (including children, e.g. all modifiers under selected groups).
186
+
187
+ Translations are persisted via the dashboard-only endpoint `PUT /api/stores/:storeId/translations/:entityType/:entityId/:locale`. The storefront SDK never calls this — it only consumes the overlay on read.
188
+
189
+ ## Troubleshooting
190
+
191
+ | Symptom | Likely cause | Fix |
192
+ | -------------------------------------------------------------- | --------------------------------------------------- | --------------------------------------------------------------------------------- |
193
+ | `product.name` is English even though locale is `he` | Store doesn't have `he` in `supportedLocales` | Add the locale in `Dashboard → Settings → Languages` |
194
+ | Some fields translate, others don't | Merchant translated subset | Per-field fallback is by design — fill the rest in dashboard |
195
+ | Layout broken in Hebrew | Missing `<html dir="rtl">` | Use `getDirection(locale)` in the layout |
196
+ | Modifier names in default language but product name translates | Merchant translated `Product` only | Bulk translate on `/products/modifier-groups` covers groups + all their modifiers |
197
+ | Custom-field label "Warranty" doesn't translate | Merchant didn't translate the `MetafieldDefinition` | Per-row "Translate" on `/products/custom-fields` |
198
+ | Bundle name "Summer Sale" stays in English in Hebrew cart | Merchant didn't translate the `BundleOffer` itself | Click the Languages icon on the bundle row in `/products/.../offers` |
199
+
200
+ See [the Brainerce docs](https://brainerce.com/docs/concepts/translations) for the canonical reference.
@@ -1,52 +1,52 @@
1
- {
2
- "name": "<%= projectName %>",
3
- "version": "0.1.0",
4
- "private": true,
5
- "scripts": {
6
- "dev": "next dev",
7
- "build": "next build",
8
- "start": "next start",
9
- "lint": "next lint",
10
- "setup": "node scripts/fetch-store-info.mjs"
11
- },
12
- "dependencies": {
13
- "brainerce": "<%= brainerceVersion %>",
14
- "next": "^15.3.4",
15
- "react": "^19.0.0",
16
- "react-dom": "^19.0.0",
17
- "@radix-ui/react-accordion": "^1.2.0",
18
- "@radix-ui/react-checkbox": "^1.1.1",
19
- "@radix-ui/react-dialog": "^1.1.2",
20
- "@radix-ui/react-label": "^2.1.0",
21
- "@radix-ui/react-radio-group": "^1.2.0",
22
- "@radix-ui/react-select": "^2.1.1",
23
- "@radix-ui/react-separator": "^1.1.0",
24
- "@radix-ui/react-slot": "^1.1.0",
25
- "@radix-ui/react-tabs": "^1.1.0",
26
- "@radix-ui/react-tooltip": "^1.1.4",
27
- "class-variance-authority": "^0.7.1",
28
- "clsx": "^2.1.1",
29
- "lucide-react": "^0.462.0",
30
- "tailwind-merge": "^2.5.2",
31
- "tailwindcss-animate": "^1.0.7",
32
- "isomorphic-dompurify": "<%= isomorphicDompurifyVersion %>",
33
- "sharp": "^0.35.3"
34
- },
35
- "devDependencies": {
36
- "@types/node": "^20.0.0",
37
- "@types/react": "^19.0.0",
38
- "@types/react-dom": "^19.0.0",
39
- "autoprefixer": "^10.4.0",
40
- "eslint": "^9.0.0",
41
- "eslint-config-next": "^15.3.4",
42
- "postcss": "^8.4.49",
43
- "tailwindcss": "^3.4.0",
44
- "typescript": "^5.4.0"
45
- },
46
- "engines": {
47
- "node": "^20.19.0 || ^22.13.0 || >=24.0.0"
48
- },
49
- "pnpm": {
50
- "onlyBuiltDependencies": ["sharp"]
51
- }
52
- }
1
+ {
2
+ "name": "<%= projectName %>",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "scripts": {
6
+ "dev": "next dev",
7
+ "build": "next build",
8
+ "start": "next start",
9
+ "lint": "next lint",
10
+ "setup": "node scripts/fetch-store-info.mjs"
11
+ },
12
+ "dependencies": {
13
+ "brainerce": "<%= brainerceVersion %>",
14
+ "next": "^15.3.4",
15
+ "react": "^19.0.0",
16
+ "react-dom": "^19.0.0",
17
+ "@radix-ui/react-accordion": "^1.2.0",
18
+ "@radix-ui/react-checkbox": "^1.1.1",
19
+ "@radix-ui/react-dialog": "^1.1.2",
20
+ "@radix-ui/react-label": "^2.1.0",
21
+ "@radix-ui/react-radio-group": "^1.2.0",
22
+ "@radix-ui/react-select": "^2.1.1",
23
+ "@radix-ui/react-separator": "^1.1.0",
24
+ "@radix-ui/react-slot": "^1.1.0",
25
+ "@radix-ui/react-tabs": "^1.1.0",
26
+ "@radix-ui/react-tooltip": "^1.1.4",
27
+ "class-variance-authority": "^0.7.1",
28
+ "clsx": "^2.1.1",
29
+ "lucide-react": "^0.462.0",
30
+ "tailwind-merge": "^2.5.2",
31
+ "tailwindcss-animate": "^1.0.7",
32
+ "isomorphic-dompurify": "<%= isomorphicDompurifyVersion %>",
33
+ "sharp": "^0.35.3"
34
+ },
35
+ "devDependencies": {
36
+ "@types/node": "^20.0.0",
37
+ "@types/react": "^19.0.0",
38
+ "@types/react-dom": "^19.0.0",
39
+ "autoprefixer": "^10.4.0",
40
+ "eslint": "^9.0.0",
41
+ "eslint-config-next": "^15.3.4",
42
+ "postcss": "^8.4.49",
43
+ "tailwindcss": "^3.4.0",
44
+ "typescript": "^5.4.0"
45
+ },
46
+ "engines": {
47
+ "node": "^20.19.0 || ^22.13.0 || >=24.0.0"
48
+ },
49
+ "pnpm": {
50
+ "onlyBuiltDependencies": ["sharp"]
51
+ }
52
+ }
@@ -0,0 +1,87 @@
1
+ /**
2
+ * agents.md — GET /agents.md
3
+ *
4
+ * The canonical AI-agent discovery file (the convention Shopify made standard
5
+ * in 2026): a machine-readable guide telling shopping agents and AI assistants
6
+ * what this site sells, where its machine surfaces live, and how a purchase
7
+ * works. /llms.txt mirrors the same information in the older convention —
8
+ * keep the two consistent.
9
+ */
10
+ import { getServerClient } from '@/core/lib/brainerce';
11
+
12
+ export const revalidate = 3600;
13
+
14
+ interface CategoryNodeLike {
15
+ name: string;
16
+ slug?: string | null;
17
+ children?: CategoryNodeLike[];
18
+ }
19
+
20
+ export async function GET() {
21
+ const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || 'https://example.com';
22
+ // Machine-readable product feed served by the Brainerce API per channel —
23
+ // the same artifact Google/Microsoft/Perplexity ingest; agents can read the
24
+ // whole catalog in one structured fetch instead of crawling page by page.
25
+ const apiUrl = (process.env.BRAINERCE_API_URL || 'https://api.brainerce.com').replace(/\/+$/, '');
26
+ const channelId =
27
+ process.env.NEXT_PUBLIC_BRAINERCE_SALES_CHANNEL_ID ||
28
+ process.env.NEXT_PUBLIC_BRAINERCE_CONNECTION_ID ||
29
+ '';
30
+ const client = getServerClient();
31
+
32
+ const [info, categoriesRes] = await Promise.all([
33
+ client.getStoreInfo().catch(() => null),
34
+ client.getCategories().catch(() => null),
35
+ ]);
36
+
37
+ const topCategories: CategoryNodeLike[] = (
38
+ (categoriesRes?.categories as CategoryNodeLike[] | undefined) ?? []
39
+ ).filter((c) => c.slug);
40
+
41
+ const lines: string[] = [
42
+ `# ${info?.name ?? 'Store'} — guide for AI agents`,
43
+ '',
44
+ ...(info?.metaDescription ? [`> ${info.metaDescription}`, ''] : []),
45
+ 'This is an e-commerce storefront. Product data, prices, and availability',
46
+ 'are rendered server-side into every page (no JavaScript execution needed)',
47
+ 'and marked up with schema.org Product / Offer / BreadcrumbList JSON-LD.',
48
+ '',
49
+ '## Machine-readable surfaces',
50
+ '',
51
+ `- Sitemap: ${baseUrl}/sitemap.xml (products, categories, articles, pages)`,
52
+ `- AI summary: ${baseUrl}/llms.txt`,
53
+ `- Blog RSS: ${baseUrl}/blog/rss.xml`,
54
+ ...(channelId
55
+ ? [
56
+ `- Product feed (Google Shopping XML): ${apiUrl}/api/feeds/${channelId}/google-shopping.xml`,
57
+ `- Product feed (JSONL): ${apiUrl}/api/feeds/${channelId}/openai-products.jsonl`,
58
+ `- MCP endpoint (catalog tools, stateless JSON-RPC): ${apiUrl}/api/mcp/storefront/${channelId}`,
59
+ ' Tools: search_products, get_product, list_categories, get_store_info. Read-only;',
60
+ ' to buy, send the shopper to the product `url` — checkout happens on this site.',
61
+ ]
62
+ : []),
63
+ `- IndexNow key: ${baseUrl}/indexnow-key.txt`,
64
+ '',
65
+ '## Key pages',
66
+ '',
67
+ `- [All products](${baseUrl}/products) — full catalog, filterable`,
68
+ ...topCategories
69
+ .slice(0, 20)
70
+ .map((c) => `- [${c.name}](${baseUrl}/category/${c.slug}) — category page`),
71
+ `- [Blog](${baseUrl}/blog) — guides and articles`,
72
+ `- [FAQ](${baseUrl}/faq)`,
73
+ `- [Contact](${baseUrl}/contact)`,
74
+ '',
75
+ '## Buying',
76
+ '',
77
+ `- Currency: ${info?.currency ?? 'see product pages'}`,
78
+ '- Checkout happens on this site (cart → checkout). Carts and checkout',
79
+ ' are session-based; agents should hand the shopper the product URL.',
80
+ ...(info?.contactEmail ? ['', `## Contact`, '', `- Email: ${info.contactEmail}`] : []),
81
+ '',
82
+ ];
83
+
84
+ return new Response(lines.join('\n'), {
85
+ headers: { 'Content-Type': 'text/markdown; charset=utf-8' },
86
+ });
87
+ }