create-brainerce-store 1.72.0 → 1.73.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.
- package/dist/index.js +4 -4
- package/messages/en.json +3 -0
- package/messages/he.json +3 -0
- package/package.json +1 -1
- package/templates/nextjs/base/TRANSLATIONS.md +207 -207
- package/templates/nextjs/base/src/app/checkout/page.tsx +1074 -1074
- package/templates/nextjs/base/src/components/account/order-history.tsx +371 -371
- package/templates/nextjs/base/src/components/account/order-status-timeline.tsx +85 -85
- package/templates/nextjs/base/src/core/hooks/use-cart-page.ts +127 -127
- package/templates/nextjs/base/src/ui/cart/cart-item.tsx +164 -164
- package/templates/nextjs/base/src/ui/cart/cart-view.tsx +176 -176
- package/templates/nextjs/base/src/ui/cart/reservation-countdown.tsx +137 -137
- package/templates/nextjs/base/src/ui/product/review-form.tsx +33 -11
- package/templates/nextjs/designs/atelier/ui/cart/cart-drawer.tsx +177 -181
- package/templates/nextjs/designs/atelier/ui/cart/cart-item.tsx +158 -158
- package/templates/nextjs/designs/atelier/ui/cart/cart-view.tsx +184 -184
- package/templates/nextjs/designs/atelier/ui/cart/reservation-countdown.tsx +131 -131
- package/templates/nextjs/designs/atelier/ui/product/review-form.tsx +30 -10
- package/templates/nextjs/ui-canvas/cart/cart-item.tsx +137 -137
- package/templates/nextjs/ui-canvas/cart/cart-view.tsx +140 -140
- package/templates/nextjs/ui-canvas/cart/reservation-countdown.tsx +124 -124
- package/templates/nextjs/ui-canvas/product/review-form.tsx +9 -1
package/dist/index.js
CHANGED
|
@@ -31,7 +31,7 @@ var require_package = __commonJS({
|
|
|
31
31
|
"package.json"(exports2, module2) {
|
|
32
32
|
module2.exports = {
|
|
33
33
|
name: "create-brainerce-store",
|
|
34
|
-
version: "1.
|
|
34
|
+
version: "1.73.0",
|
|
35
35
|
description: "Scaffold a production-ready e-commerce storefront connected to Brainerce",
|
|
36
36
|
bin: {
|
|
37
37
|
"create-brainerce-store": "dist/index.js"
|
|
@@ -235,7 +235,7 @@ var BRAINERCE_RUNTIME_DEPS = Object.freeze({
|
|
|
235
235
|
// missing keys. 2.0 also stops forgotPassword sending `resetUrl`, which the
|
|
236
236
|
// API rejects with 400 under forbidNonWhitelisted, so a scaffold on 1.x has
|
|
237
237
|
// a broken password reset in the browser.
|
|
238
|
-
brainerce: "^2.0.
|
|
238
|
+
brainerce: "^2.0.1",
|
|
239
239
|
"isomorphic-dompurify": "^3.8.0"
|
|
240
240
|
});
|
|
241
241
|
|
|
@@ -976,13 +976,13 @@ program.name("create-brainerce-store").description("Scaffold a production-ready
|
|
|
976
976
|
const channelSlug = storeInfo ? slugify(storeInfo.name) : "";
|
|
977
977
|
const storeSlug = storeInfo ? slugify(storeInfo.storeName) : "";
|
|
978
978
|
const projectNameSuggestion = channelSlug || storeSlug || void 0;
|
|
979
|
-
|
|
979
|
+
language = language || storeInfo?.language;
|
|
980
980
|
if (!projectName || !connectionId || !language) {
|
|
981
981
|
const answers = await runInteractive({
|
|
982
982
|
projectName,
|
|
983
983
|
projectNameSuggestion,
|
|
984
984
|
connectionId,
|
|
985
|
-
language
|
|
985
|
+
language,
|
|
986
986
|
framework,
|
|
987
987
|
design,
|
|
988
988
|
pkgManager,
|
package/messages/en.json
CHANGED
|
@@ -154,6 +154,9 @@
|
|
|
154
154
|
"submitReview": "Submit review",
|
|
155
155
|
"addPhotos": "Add photos",
|
|
156
156
|
"removePhoto": "Remove photo",
|
|
157
|
+
"choosePhotos": "Choose photos",
|
|
158
|
+
"photoFormats": "JPG, PNG, WebP or GIF, up to {mb}MB each.",
|
|
159
|
+
"photoLimitReached": "That is all {max} photos. Remove one to add another.",
|
|
157
160
|
"photoUploading": "Uploading…",
|
|
158
161
|
"photoTooLarge": "That photo is too large. Please pick a smaller one.",
|
|
159
162
|
"photoUploadFailed": "Could not upload that photo. Please try another.",
|
package/messages/he.json
CHANGED
|
@@ -154,6 +154,9 @@
|
|
|
154
154
|
"submitReview": "שליחת ביקורת",
|
|
155
155
|
"addPhotos": "הוספת תמונות",
|
|
156
156
|
"removePhoto": "הסרת תמונה",
|
|
157
|
+
"choosePhotos": "בחירת תמונות",
|
|
158
|
+
"photoFormats": "JPG, PNG, WebP או GIF, עד {mb}MB לכל תמונה.",
|
|
159
|
+
"photoLimitReached": "אלה כל {max} התמונות. הסר אחת כדי להוסיף אחרת.",
|
|
157
160
|
"photoUploading": "מעלה...",
|
|
158
161
|
"photoTooLarge": "התמונה הזו גדולה מדי. בחרו תמונה קטנה יותר.",
|
|
159
162
|
"photoUploadFailed": "לא הצלחנו להעלות את התמונה. נסו תמונה אחרת.",
|
package/package.json
CHANGED
|
@@ -1,207 +1,207 @@
|
|
|
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, so 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. `getCheckoutBumps` takes a CHECKOUT id, not a cart id:
|
|
169
|
-
const { bumps } = await client.getCheckoutBumps(checkoutId);
|
|
170
|
-
bumps[0].title; // translated bump headline (or merchant override)
|
|
171
|
-
bumps[0].bumpProduct.name; // translated product name
|
|
172
|
-
|
|
173
|
-
// Adding or removing a bump takes the CART id plus the bump config id
|
|
174
|
-
// (bumps[i].id). Pass a variantId when bumps[i].requiresVariantSelection:
|
|
175
|
-
await client.addOrderBump(cartId, bumps[0].id, selectedVariantId);
|
|
176
|
-
await client.removeOrderBump(cartId, bumps[0].id);
|
|
177
|
-
|
|
178
|
-
// Discount-rule banners. The API returns ready-to-render banner text, not the
|
|
179
|
-
// rule object: DiscountBanner is { ruleId, text, type }, so there is no `name`
|
|
180
|
-
// and no `displayConfig` to compose yourself.
|
|
181
|
-
const banners = await client.getDiscountBanners();
|
|
182
|
-
banners[0].text; // translated banner copy
|
|
183
|
-
```
|
|
184
|
-
|
|
185
|
-
## How merchants populate translations
|
|
186
|
-
|
|
187
|
-
For background — your storefront doesn't need to call these endpoints, but knowing the merchant flow helps when debugging unexpectedly-empty translations:
|
|
188
|
-
|
|
189
|
-
- **Per-row overlay** on every taxonomy/product list page in the dashboard.
|
|
190
|
-
- **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).
|
|
191
|
-
- **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.
|
|
192
|
-
- **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).
|
|
193
|
-
|
|
194
|
-
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.
|
|
195
|
-
|
|
196
|
-
## Troubleshooting
|
|
197
|
-
|
|
198
|
-
| Symptom | Likely cause | Fix |
|
|
199
|
-
| -------------------------------------------------------------- | --------------------------------------------------- | --------------------------------------------------------------------------------- |
|
|
200
|
-
| `product.name` is English even though locale is `he` | Store doesn't have `he` in `supportedLocales` | Add the locale in `Dashboard → Settings → Languages` |
|
|
201
|
-
| Some fields translate, others don't | Merchant translated subset | Per-field fallback is by design — fill the rest in dashboard |
|
|
202
|
-
| Layout broken in Hebrew | Missing `<html dir="rtl">` | Use `getDirection(locale)` in the layout |
|
|
203
|
-
| Modifier names in default language but product name translates | Merchant translated `Product` only | Bulk translate on `/products/modifier-groups` covers groups + all their modifiers |
|
|
204
|
-
| Custom-field label "Warranty" doesn't translate | Merchant didn't translate the `MetafieldDefinition` | Per-row "Translate" on `/products/custom-fields` |
|
|
205
|
-
| 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` |
|
|
206
|
-
|
|
207
|
-
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, so 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. `getCheckoutBumps` takes a CHECKOUT id, not a cart id:
|
|
169
|
+
const { bumps } = await client.getCheckoutBumps(checkoutId);
|
|
170
|
+
bumps[0].title; // translated bump headline (or merchant override)
|
|
171
|
+
bumps[0].bumpProduct.name; // translated product name
|
|
172
|
+
|
|
173
|
+
// Adding or removing a bump takes the CART id plus the bump config id
|
|
174
|
+
// (bumps[i].id). Pass a variantId when bumps[i].requiresVariantSelection:
|
|
175
|
+
await client.addOrderBump(cartId, bumps[0].id, selectedVariantId);
|
|
176
|
+
await client.removeOrderBump(cartId, bumps[0].id);
|
|
177
|
+
|
|
178
|
+
// Discount-rule banners. The API returns ready-to-render banner text, not the
|
|
179
|
+
// rule object: DiscountBanner is { ruleId, text, type }, so there is no `name`
|
|
180
|
+
// and no `displayConfig` to compose yourself.
|
|
181
|
+
const banners = await client.getDiscountBanners();
|
|
182
|
+
banners[0].text; // translated banner copy
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
## How merchants populate translations
|
|
186
|
+
|
|
187
|
+
For background — your storefront doesn't need to call these endpoints, but knowing the merchant flow helps when debugging unexpectedly-empty translations:
|
|
188
|
+
|
|
189
|
+
- **Per-row overlay** on every taxonomy/product list page in the dashboard.
|
|
190
|
+
- **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).
|
|
191
|
+
- **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.
|
|
192
|
+
- **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).
|
|
193
|
+
|
|
194
|
+
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.
|
|
195
|
+
|
|
196
|
+
## Troubleshooting
|
|
197
|
+
|
|
198
|
+
| Symptom | Likely cause | Fix |
|
|
199
|
+
| -------------------------------------------------------------- | --------------------------------------------------- | --------------------------------------------------------------------------------- |
|
|
200
|
+
| `product.name` is English even though locale is `he` | Store doesn't have `he` in `supportedLocales` | Add the locale in `Dashboard → Settings → Languages` |
|
|
201
|
+
| Some fields translate, others don't | Merchant translated subset | Per-field fallback is by design — fill the rest in dashboard |
|
|
202
|
+
| Layout broken in Hebrew | Missing `<html dir="rtl">` | Use `getDirection(locale)` in the layout |
|
|
203
|
+
| Modifier names in default language but product name translates | Merchant translated `Product` only | Bulk translate on `/products/modifier-groups` covers groups + all their modifiers |
|
|
204
|
+
| Custom-field label "Warranty" doesn't translate | Merchant didn't translate the `MetafieldDefinition` | Per-row "Translate" on `/products/custom-fields` |
|
|
205
|
+
| 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` |
|
|
206
|
+
|
|
207
|
+
See [the Brainerce docs](https://brainerce.com/docs/concepts/translations) for the canonical reference.
|