@base44/app-plugin-commerce 0.7.1 → 0.8.1

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 (82) hide show
  1. package/README.md +4 -10
  2. package/base44/entities/commerce.ProductReview.jsonc +2 -2
  3. package/base44/functions/commerce/storefront-catalog/entry.ts +7 -3
  4. package/package.json +1 -1
  5. package/scripts/install.js +2 -4
  6. package/skills/commerce/SKILL.md +9 -3
  7. package/skills/commerce/docs/api-storefront.md +3 -3
  8. package/skills/commerce/install/01-install.md +8 -24
  9. package/skills/commerce/install/02-storefront.md +22 -9
  10. package/skills/commerce/references/admin-localization.md +95 -0
  11. package/skills/commerce/references/catalog-rendering.md +1 -1
  12. package/skills/commerce/references/reviews.md +7 -6
  13. package/src/commerce/admin/README.md +36 -14
  14. package/src/commerce/admin/bot/StoreAdminBot.jsx +12 -11
  15. package/src/commerce/admin/components/AddressForm.jsx +13 -12
  16. package/src/commerce/admin/components/ConfirmDialog.jsx +5 -4
  17. package/src/commerce/admin/components/CountrySelect.jsx +12 -8
  18. package/src/commerce/admin/components/DataTable.jsx +3 -2
  19. package/src/commerce/admin/components/DateRangePicker.jsx +11 -10
  20. package/src/commerce/admin/components/EmptyState.jsx +2 -1
  21. package/src/commerce/admin/components/MediaUploader.jsx +3 -2
  22. package/src/commerce/admin/components/MetaDataEditor.jsx +4 -3
  23. package/src/commerce/admin/components/SearchSelect.jsx +4 -3
  24. package/src/commerce/admin/context/SettingsContext.jsx +12 -25
  25. package/src/commerce/admin/hooks/useMoney.js +3 -2
  26. package/src/commerce/admin/i18n/index.js +54 -0
  27. package/src/commerce/admin/i18n/locales/de.js +896 -0
  28. package/src/commerce/admin/i18n/locales/en.js +900 -0
  29. package/src/commerce/admin/i18n/locales/es.js +896 -0
  30. package/src/commerce/admin/i18n/locales/fr.js +896 -0
  31. package/src/commerce/admin/i18n/locales/ja.js +896 -0
  32. package/src/commerce/admin/i18n/locales/pt.js +896 -0
  33. package/src/commerce/admin/index.jsx +6 -30
  34. package/src/commerce/admin/layout/AccessDenied.jsx +8 -9
  35. package/src/commerce/admin/layout/AuthGuard.jsx +2 -1
  36. package/src/commerce/admin/layout/Sidebar.jsx +29 -18
  37. package/src/commerce/admin/layout/Topbar.jsx +4 -3
  38. package/src/commerce/admin/lib/api.js +3 -2
  39. package/src/commerce/admin/lib/constants.js +44 -43
  40. package/src/commerce/admin/lib/format.js +4 -3
  41. package/src/commerce/admin/lib/paths.js +2 -3
  42. package/src/commerce/admin/pages/Dashboard.jsx +25 -24
  43. package/src/commerce/admin/pages/coupons/CouponEditor.jsx +55 -57
  44. package/src/commerce/admin/pages/coupons/CouponsList.jsx +22 -21
  45. package/src/commerce/admin/pages/customers/CustomerEditor.jsx +31 -30
  46. package/src/commerce/admin/pages/customers/CustomersList.jsx +19 -18
  47. package/src/commerce/admin/pages/orders/OrderEditor.jsx +75 -71
  48. package/src/commerce/admin/pages/orders/OrdersList.jsx +16 -15
  49. package/src/commerce/admin/pages/orders/components/AddProductDialog.jsx +12 -11
  50. package/src/commerce/admin/pages/orders/components/DownloadPermissionsPanel.jsx +14 -13
  51. package/src/commerce/admin/pages/orders/components/LineItemsTable.jsx +12 -11
  52. package/src/commerce/admin/pages/orders/components/OrderNotesPanel.jsx +11 -10
  53. package/src/commerce/admin/pages/orders/components/PaymentPanel.jsx +22 -22
  54. package/src/commerce/admin/pages/orders/components/RefundPanel.jsx +18 -17
  55. package/src/commerce/admin/pages/orders/components/TotalsBox.jsx +18 -17
  56. package/src/commerce/admin/pages/products/Categories.jsx +14 -13
  57. package/src/commerce/admin/pages/products/ProductEditor.jsx +16 -15
  58. package/src/commerce/admin/pages/products/ProductsList.jsx +41 -36
  59. package/src/commerce/admin/pages/products/Reviews.jsx +54 -46
  60. package/src/commerce/admin/pages/products/components/AttributesSection.jsx +45 -39
  61. package/src/commerce/admin/pages/products/components/ProductDataPanel.jsx +8 -7
  62. package/src/commerce/admin/pages/products/components/PublishBox.jsx +10 -9
  63. package/src/commerce/admin/pages/products/components/TaxonomyPanel.jsx +18 -17
  64. package/src/commerce/admin/pages/products/components/tabs/DownloadsTab.jsx +11 -10
  65. package/src/commerce/admin/pages/products/components/tabs/LinkedTab.jsx +9 -10
  66. package/src/commerce/admin/pages/products/components/tabs/ModifiersTab.jsx +3 -9
  67. package/src/commerce/admin/pages/products/components/tabs/PriceInventoryTab.jsx +76 -78
  68. package/src/commerce/admin/pages/reports/Reports.jsx +33 -32
  69. package/src/commerce/admin/pages/settings/EmailsSettings.jsx +36 -39
  70. package/src/commerce/admin/pages/settings/GeneralSettings.jsx +13 -14
  71. package/src/commerce/admin/pages/settings/InventorySettings.jsx +12 -13
  72. package/src/commerce/admin/pages/settings/LocationEditor.jsx +48 -47
  73. package/src/commerce/admin/pages/settings/PaymentsSettings.jsx +32 -33
  74. package/src/commerce/admin/pages/settings/SettingsLayout.jsx +13 -12
  75. package/src/commerce/admin/pages/settings/ShippingTaxSettings.jsx +31 -31
  76. package/src/commerce/admin/pages/settings/useGroupForm.jsx +4 -3
  77. package/src/commerce/admin/pages/status/WebhookEditor.jsx +34 -31
  78. package/src/commerce/admin/pages/status/Webhooks.jsx +11 -10
  79. package/src/commerce/admin/routes.jsx +31 -43
  80. package/src/commerce/storefront/pickers.jsx +19 -5
  81. package/src/commerce/storefront/useCheckout.jsx +28 -3
  82. package/src/commerce/utils/storefront.js +49 -2
@@ -16,6 +16,7 @@ import { ChevronDown, ChevronRight, Copy, Trash2, TriangleAlert } from "lucide-r
16
16
  import { toast } from "sonner";
17
17
 
18
18
  import MoneyInput from "../../../../components/MoneyInput";
19
+ import { t } from "../../../../i18n";
19
20
  import MediaUploader from "../../../../components/MediaUploader";
20
21
  import ConfirmDialog from "../../../../components/ConfirmDialog";
21
22
  import useMoney from "../../../../hooks/useMoney";
@@ -36,11 +37,11 @@ const fromDateInput = (v) => (v ? new Date(`${v}T00:00:00Z`).toISOString() : nul
36
37
  * and the attribute combination are what makes a variant itself, so they stay.
37
38
  */
38
39
  const COPY_GROUPS = [
39
- { key: "price", label: "Price", fields: ["regular_price", "sale_price", "date_on_sale_from", "date_on_sale_to"] },
40
- { key: "inventory", label: "Inventory", fields: ["manage_stock", "stock_quantity", "backorders", "stock_status"] },
40
+ { key: "price", label: t("products.section_price"), fields: ["regular_price", "sale_price", "date_on_sale_from", "date_on_sale_to"] },
41
+ { key: "inventory", label: t("products.section_inventory"), fields: ["manage_stock", "stock_quantity", "backorders", "stock_status"] },
41
42
  {
42
43
  key: "shipping",
43
- label: "Shipping",
44
+ label: t("products.section_shipping"),
44
45
  fields: ["weight", "dimensions", "virtual", "downloadable"],
45
46
  },
46
47
  ];
@@ -77,12 +78,12 @@ function PriceFields({ values, set, hideRegular = false }) {
77
78
  <>
78
79
  {!hideRegular && (
79
80
  <div className="space-y-1.5">
80
- <Label className="text-xs">Regular price</Label>
81
+ <Label className="text-xs">{t("products.regular_price")}</Label>
81
82
  <MoneyInput value={values.regular_price} onChange={(v) => set({ regular_price: v })} />
82
83
  </div>
83
84
  )}
84
85
  <div className="space-y-1.5">
85
- <Label className="text-xs">Sale price</Label>
86
+ <Label className="text-xs">{t("products.sale_price")}</Label>
86
87
  <MoneyInput value={values.sale_price} onChange={(v) => set({ sale_price: v })} />
87
88
  </div>
88
89
  <div className="sm:col-span-2">
@@ -97,13 +98,13 @@ function PriceFields({ values, set, hideRegular = false }) {
97
98
  )
98
99
  }
99
100
  />
100
- Schedule sale
101
+ {t("products.schedule_sale")}
101
102
  </label>
102
103
  </div>
103
104
  {scheduled && (
104
105
  <>
105
106
  <div className="space-y-1.5">
106
- <Label className="text-xs">Sale start</Label>
107
+ <Label className="text-xs">{t("products.sale_start")}</Label>
107
108
  <Input
108
109
  type="date"
109
110
  value={toDateInput(values.date_on_sale_from)}
@@ -111,7 +112,7 @@ function PriceFields({ values, set, hideRegular = false }) {
111
112
  />
112
113
  </div>
113
114
  <div className="space-y-1.5">
114
- <Label className="text-xs">Sale end</Label>
115
+ <Label className="text-xs">{t("products.sale_end")}</Label>
115
116
  <Input
116
117
  type="date"
117
118
  value={toDateInput(values.date_on_sale_to)}
@@ -140,7 +141,7 @@ function ShippingFields({ values, set, units }) {
140
141
  return (
141
142
  <>
142
143
  <div className="space-y-1.5">
143
- <Label className="text-xs">Weight ({units.weightUnit})</Label>
144
+ <Label className="text-xs">{t("products.weight_with_unit", { unit: units.weightUnit })}</Label>
144
145
  <Input
145
146
  type="number"
146
147
  step="any"
@@ -149,7 +150,7 @@ function ShippingFields({ values, set, units }) {
149
150
  />
150
151
  </div>
151
152
  <div className="space-y-1.5">
152
- <Label className="text-xs">Dimensions L×W×H ({units.dimensionUnit})</Label>
153
+ <Label className="text-xs">{t("products.dimensions_with_unit", { unit: units.dimensionUnit })}</Label>
153
154
  <div className="grid grid-cols-3 gap-1">
154
155
  {["length", "width", "height"].map((k) => (
155
156
  <Input
@@ -176,7 +177,7 @@ function DownloadsFields({ values, set }) {
176
177
  <div key={i} className="flex items-center gap-2">
177
178
  <Input
178
179
  className="w-48"
179
- placeholder="Name"
180
+ placeholder={t("common.name")}
180
181
  value={d.name || ""}
181
182
  onChange={(e) =>
182
183
  set({ downloads: downloads.map((x, j) => (j === i ? { ...x, name: e.target.value } : x)) })
@@ -184,7 +185,7 @@ function DownloadsFields({ values, set }) {
184
185
  />
185
186
  <Input
186
187
  className="flex-1"
187
- placeholder="File URL"
188
+ placeholder={t("products.file_url")}
188
189
  value={d.file_url || ""}
189
190
  onChange={(e) =>
190
191
  set({ downloads: downloads.map((x, j) => (j === i ? { ...x, file_url: e.target.value } : x)) })
@@ -219,15 +220,15 @@ function DownloadsFields({ values, set }) {
219
220
  function SingleProductFields({ product, up, units }) {
220
221
  return (
221
222
  <div className="grid max-w-2xl gap-4 sm:grid-cols-2">
222
- <Section label="Price">
223
+ <Section label={t("products.section_price")}>
223
224
  <PriceFields values={product} set={up} />
224
225
  </Section>
225
226
 
226
- <Section label="Inventory">
227
+ <Section label={t("products.section_inventory")}>
227
228
  <div className="space-y-1.5">
228
- <Label className="text-xs">SKU</Label>
229
+ <Label className="text-xs">{t("products.sku")}</Label>
229
230
  <Input
230
- placeholder="Unique stock keeping unit"
231
+ placeholder={t("products.sku_placeholder")}
231
232
  value={product.sku || ""}
232
233
  onChange={(e) => up({ sku: e.target.value })}
233
234
  />
@@ -239,13 +240,13 @@ function SingleProductFields({ product, up, units }) {
239
240
  checked={product.manage_stock}
240
241
  onCheckedChange={(v) => up({ manage_stock: Boolean(v) })}
241
242
  />
242
- Track stock quantity
243
+ {t("products.track_stock_quantity")}
243
244
  </label>
244
245
  </div>
245
246
  {product.manage_stock ? (
246
247
  <>
247
248
  <div className="space-y-1.5">
248
- <Label className="text-xs">Stock quantity</Label>
249
+ <Label className="text-xs">{t("products.stock_quantity")}</Label>
249
250
  <Input
250
251
  type="number"
251
252
  value={product.stock_quantity ?? ""}
@@ -253,7 +254,7 @@ function SingleProductFields({ product, up, units }) {
253
254
  />
254
255
  </div>
255
256
  <div className="space-y-1.5">
256
- <Label className="text-xs">Backorders</Label>
257
+ <Label className="text-xs">{t("products.backorders")}</Label>
257
258
  <Select value={product.backorders || "no"} onValueChange={(v) => up({ backorders: v })}>
258
259
  <SelectTrigger><SelectValue /></SelectTrigger>
259
260
  <SelectContent>
@@ -264,10 +265,10 @@ function SingleProductFields({ product, up, units }) {
264
265
  </Select>
265
266
  </div>
266
267
  <div className="space-y-1.5">
267
- <Label className="text-xs">Low stock threshold</Label>
268
+ <Label className="text-xs">{t("products.low_stock_threshold")}</Label>
268
269
  <Input
269
270
  type="number"
270
- placeholder="Store default"
271
+ placeholder={t("products.store_default")}
271
272
  value={product.low_stock_amount ?? ""}
272
273
  onChange={(e) => up({ low_stock_amount: num(e.target.value) })}
273
274
  />
@@ -275,7 +276,7 @@ function SingleProductFields({ product, up, units }) {
275
276
  </>
276
277
  ) : (
277
278
  <div className="space-y-1.5">
278
- <Label className="text-xs">Stock status</Label>
279
+ <Label className="text-xs">{t("products.stock_status")}</Label>
279
280
  <Select value={product.stock_status || "instock"} onValueChange={(v) => up({ stock_status: v })}>
280
281
  <SelectTrigger><SelectValue /></SelectTrigger>
281
282
  <SelectContent>
@@ -289,7 +290,7 @@ function SingleProductFields({ product, up, units }) {
289
290
  </Section>
290
291
 
291
292
  {!product.virtual && (
292
- <Section label="Shipping">
293
+ <Section label={t("products.section_shipping")}>
293
294
  <ShippingFields values={product} set={up} units={units} />
294
295
  </Section>
295
296
  )}
@@ -301,28 +302,28 @@ function SingleProductFields({ product, up, units }) {
301
302
  function VariantDetails({ variation, set, units, virtual }) {
302
303
  return (
303
304
  <div className="grid gap-4 sm:grid-cols-[140px_1fr]">
304
- <MediaUploader value={variation.image || null} onChange={(image) => set({ image })} label="Image" />
305
+ <MediaUploader value={variation.image || null} onChange={(image) => set({ image })} label={t("products.image")} />
305
306
 
306
307
  <div className="grid gap-3 sm:grid-cols-2">
307
- <Section label="Sale">
308
+ <Section label={t("products.section_sale")}>
308
309
  <PriceFields values={variation} set={set} hideRegular />
309
310
  </Section>
310
311
 
311
- <Section label="Inventory">
312
+ <Section label={t("products.section_inventory")}>
312
313
  <div className="space-y-1.5">
313
- <Label className="text-xs">Stock management</Label>
314
+ <Label className="text-xs">{t("products.stock_management")}</Label>
314
315
  <Select value={variation.manage_stock || "parent"} onValueChange={(v) => set({ manage_stock: v })}>
315
316
  <SelectTrigger><SelectValue /></SelectTrigger>
316
317
  <SelectContent>
317
- <SelectItem value="parent">Same as product</SelectItem>
318
- <SelectItem value="yes">Track quantity</SelectItem>
319
- <SelectItem value="no">Don't track</SelectItem>
318
+ <SelectItem value="parent">{t("products.stock_same_as_product")}</SelectItem>
319
+ <SelectItem value="yes">{t("products.stock_track_quantity")}</SelectItem>
320
+ <SelectItem value="no">{t("products.stock_dont_track")}</SelectItem>
320
321
  </SelectContent>
321
322
  </Select>
322
323
  </div>
323
324
  {variation.manage_stock === "yes" && (
324
325
  <div className="space-y-1.5">
325
- <Label className="text-xs">Backorders</Label>
326
+ <Label className="text-xs">{t("products.backorders")}</Label>
326
327
  <Select value={variation.backorders || "no"} onValueChange={(v) => set({ backorders: v })}>
327
328
  <SelectTrigger><SelectValue /></SelectTrigger>
328
329
  <SelectContent>
@@ -336,29 +337,29 @@ function VariantDetails({ variation, set, units, virtual }) {
336
337
  </Section>
337
338
 
338
339
  {!virtual && (
339
- <Section label="Shipping">
340
+ <Section label={t("products.section_shipping")}>
340
341
  <ShippingFields values={variation} set={set} units={units} />
341
342
  </Section>
342
343
  )}
343
344
 
344
- <Section label="Type">
345
+ <Section label={t("products.section_type")}>
345
346
  <div className="flex items-end gap-4 text-sm">
346
347
  <label className="flex items-center gap-1.5">
347
348
  <Checkbox checked={variation.virtual} onCheckedChange={(v) => set({ virtual: Boolean(v) })} />
348
- Virtual
349
+ {t("products.virtual")}
349
350
  </label>
350
351
  <label className="flex items-center gap-1.5">
351
352
  <Checkbox
352
353
  checked={variation.downloadable}
353
354
  onCheckedChange={(v) => set({ downloadable: Boolean(v) })}
354
355
  />
355
- Downloadable
356
+ {t("products.downloadable")}
356
357
  </label>
357
358
  </div>
358
359
  </Section>
359
360
 
360
361
  {variation.downloadable && (
361
- <Section label="Files">
362
+ <Section label={t("products.section_files")}>
362
363
  <DownloadsFields values={variation} set={set} />
363
364
  </Section>
364
365
  )}
@@ -379,18 +380,18 @@ function TaxRules({ product, up, taxGroups }) {
379
380
  onClick={() => setOpen((o) => !o)}
380
381
  >
381
382
  {open ? <ChevronDown className="h-4 w-4" /> : <ChevronRight className="h-4 w-4" />}
382
- Tax &amp; purchase rules
383
+ {t("products.tax_purchase_rules")}
383
384
  {!open && (
384
385
  <span className="font-normal text-xs">
385
386
  · {taxLabel} · {product.tax_group || "Products"}
386
- {product.sold_individually ? " · max 1 per order" : ""}
387
+ {product.sold_individually ? ` · ${t("products.max_one_per_order")}` : ""}
387
388
  </span>
388
389
  )}
389
390
  </button>
390
391
  {open && (
391
392
  <div className="mt-3 grid max-w-md gap-3 sm:grid-cols-2">
392
393
  <div className="space-y-1.5">
393
- <Label>Tax status</Label>
394
+ <Label>{t("products.tax_status")}</Label>
394
395
  <Select value={product.tax_status} onValueChange={(v) => up({ tax_status: v })}>
395
396
  <SelectTrigger><SelectValue /></SelectTrigger>
396
397
  <SelectContent>
@@ -401,7 +402,7 @@ function TaxRules({ product, up, taxGroups }) {
401
402
  </Select>
402
403
  </div>
403
404
  <div className="space-y-1.5">
404
- <Label>Tax group</Label>
405
+ <Label>{t("products.tax_group")}</Label>
405
406
  <Select value={product.tax_group || "Products"} onValueChange={(v) => up({ tax_group: v })}>
406
407
  <SelectTrigger><SelectValue /></SelectTrigger>
407
408
  <SelectContent>
@@ -416,7 +417,7 @@ function TaxRules({ product, up, taxGroups }) {
416
417
  checked={product.sold_individually}
417
418
  onCheckedChange={(v) => up({ sold_individually: Boolean(v) })}
418
419
  />
419
- Limit purchases to 1 item per order
420
+ {t("products.sold_individually")}
420
421
  </label>
421
422
  </div>
422
423
  )}
@@ -555,7 +556,7 @@ export default function PriceInventoryTab({ product, up, refs, variations, setVa
555
556
 
556
557
  const applyBulkPrice = () => {
557
558
  setVariations(variations.map((v, i) => (selected.includes(i) ? { ...v, regular_price: bulkPrice } : v)));
558
- toast.success(`Price set on ${selected.length} variant(s).`);
559
+ toast.success(t("products.bulk_price_set", { count: selected.length }));
559
560
  };
560
561
  const applyBulkStock = () => {
561
562
  const qty = Number(bulkStock);
@@ -564,7 +565,7 @@ export default function PriceInventoryTab({ product, up, refs, variations, setVa
564
565
  selected.includes(i) ? { ...v, manage_stock: "yes", stock_quantity: qty } : v
565
566
  )
566
567
  );
567
- toast.success(`Stock set on ${selected.length} variant(s).`);
568
+ toast.success(t("products.bulk_stock_set", { count: selected.length }));
568
569
  };
569
570
  /** Push one variant's price, inventory and shipping onto the selected rows. */
570
571
  const applyCopy = () => {
@@ -579,7 +580,7 @@ export default function PriceInventoryTab({ product, up, refs, variations, setVa
579
580
  return { ...v, ...patch };
580
581
  })
581
582
  );
582
- toast.success(`Copied to ${selected.filter((i) => i !== Number(copyFrom)).length} variant(s).`);
583
+ toast.success(t("products.bulk_copied", { count: selected.filter((i) => i !== Number(copyFrom)).length }));
583
584
  };
584
585
 
585
586
  const switchToSingle = () => {
@@ -601,16 +602,16 @@ export default function PriceInventoryTab({ product, up, refs, variations, setVa
601
602
  <div>
602
603
  <div className="inline-flex rounded-lg border bg-muted/60 p-0.5">
603
604
  <button type="button" className={modeBtn(mode === "single")} onClick={switchToSingle}>
604
- Single product
605
+ {t("products.single_product")}
605
606
  </button>
606
607
  <button type="button" className={modeBtn(mode === "variants")} onClick={() => setMode("variants")}>
607
- Has variants
608
+ {t("products.has_variants")}
608
609
  </button>
609
610
  </div>
610
611
  <p className="mt-1.5 text-xs text-muted-foreground">
611
612
  {mode === "single"
612
- ? "One price, one stock count. Switch if it comes in sizes, packs or colors."
613
- : "Each combination of option values becomes a variant with its own price, stock and SKU."}
613
+ ? t("products.single_hint")
614
+ : t("products.variants_hint")}
614
615
  </p>
615
616
  </div>
616
617
 
@@ -624,33 +625,33 @@ export default function PriceInventoryTab({ product, up, refs, variations, setVa
624
625
  <div className="space-y-2 border-t pt-4">
625
626
  <div className="flex flex-wrap items-center justify-between gap-2">
626
627
  <span className="text-sm font-semibold">
627
- {variations.length} variant{variations.length === 1 ? "" : "s"}
628
+ {t("products.variant_count", { count: variations.length })}
628
629
  </span>
629
630
  <span className="text-xs text-muted-foreground">
630
- Store price:{" "}
631
+ {t("products.store_price")}{" "}
631
632
  <span className="font-semibold text-foreground">
632
- {fromPrice != null ? `from ${money.format(fromPrice)}` : "—"}
633
+ {fromPrice != null ? t("products.from_price", { price: money.format(fromPrice) }) : "—"}
633
634
  </span>{" "}
634
- (lowest enabled variant)
635
+ {t("products.lowest_enabled_variant")}
635
636
  </span>
636
637
  </div>
637
638
 
638
639
  {selected.length > 0 && (
639
640
  <div className="flex flex-wrap items-center gap-2 rounded-md border bg-muted/40 p-2 text-sm">
640
- <span className="font-medium">{selected.length} selected</span>
641
+ <span className="font-medium">{t("common.selected_count", { count: selected.length })}</span>
641
642
  <MoneyInput className="w-32" value={bulkPrice} onChange={setBulkPrice} />
642
643
  <Button type="button" variant="outline" size="sm" disabled={bulkPrice == null} onClick={applyBulkPrice}>
643
- Set price
644
+ {t("products.set_price")}
644
645
  </Button>
645
646
  <Input
646
647
  type="number"
647
648
  className="h-9 w-24"
648
- placeholder="Qty"
649
+ placeholder={t("products.qty")}
649
650
  value={bulkStock}
650
651
  onChange={(e) => setBulkStock(e.target.value)}
651
652
  />
652
653
  <Button type="button" variant="outline" size="sm" disabled={bulkStock === ""} onClick={applyBulkStock}>
653
- Set stock
654
+ {t("products.set_stock")}
654
655
  </Button>
655
656
  <div className="flex items-center gap-1.5">
656
657
  <Copy className="h-4 w-4 shrink-0 text-muted-foreground" />
@@ -665,11 +666,11 @@ export default function PriceInventoryTab({ product, up, refs, variations, setVa
665
666
  </SelectContent>
666
667
  </Select>
667
668
  <Button type="button" variant="outline" size="sm" onClick={applyCopy}>
668
- Copy to selected
669
+ {t("products.copy_to_selected")}
669
670
  </Button>
670
671
  </div>
671
672
  <Button type="button" variant="ghost" size="sm" className="ml-auto" onClick={() => setSelected([])}>
672
- Clear
673
+ {t("products.clear")}
673
674
  </Button>
674
675
  </div>
675
676
  )}
@@ -677,11 +678,8 @@ export default function PriceInventoryTab({ product, up, refs, variations, setVa
677
678
  {variations.length === 0 ? (
678
679
  <Alert>
679
680
  <TriangleAlert className="h-4 w-4" />
680
- <AlertTitle>No variants yet — customers can't buy this product</AlertTitle>
681
- <AlertDescription>
682
- Pick the values this product comes in for the option above and its variants
683
- appear here automatically.
684
- </AlertDescription>
681
+ <AlertTitle>{t("products.no_variants_title")}</AlertTitle>
682
+ <AlertDescription>{t("products.no_variants_description")}</AlertDescription>
685
683
  </Alert>
686
684
  ) : (
687
685
  <div className="overflow-x-auto rounded-md border">
@@ -692,15 +690,15 @@ export default function PriceInventoryTab({ product, up, refs, variations, setVa
692
690
  <Checkbox
693
691
  checked={allSelected}
694
692
  onCheckedChange={(v) => setSelected(v ? variations.map((_, i) => i) : [])}
695
- aria-label="Select all variants"
693
+ aria-label={t("products.select_all_variants")}
696
694
  />
697
695
  </th>
698
- <th className="p-2 font-semibold">Variant</th>
699
- <th className="w-36 p-2 font-semibold">Price</th>
700
- <th className="w-24 p-2 font-semibold">Stock</th>
701
- <th className="w-48 p-2 font-semibold">SKU</th>
702
- <th className="w-16 p-2 text-center font-semibold">Default</th>
703
- <th className="w-20 p-2 text-center font-semibold">Enabled</th>
696
+ <th className="p-2 font-semibold">{t("products.variant")}</th>
697
+ <th className="w-36 p-2 font-semibold">{t("products.price")}</th>
698
+ <th className="w-24 p-2 font-semibold">{t("products.stock")}</th>
699
+ <th className="w-48 p-2 font-semibold">{t("products.sku")}</th>
700
+ <th className="w-16 p-2 text-center font-semibold">{t("products.default")}</th>
701
+ <th className="w-20 p-2 text-center font-semibold">{t("products.enabled")}</th>
704
702
  </tr>
705
703
  </thead>
706
704
  <tbody>
@@ -711,7 +709,7 @@ export default function PriceInventoryTab({ product, up, refs, variations, setVa
711
709
  <Checkbox
712
710
  checked={selected.includes(i)}
713
711
  onCheckedChange={(on) => toggleSelect(i, Boolean(on))}
714
- aria-label={`Select ${variationLabel(v)}`}
712
+ aria-label={t("products.select_variant", { name: variationLabel(v) })}
715
713
  />
716
714
  </td>
717
715
  <td className="p-2">
@@ -719,7 +717,7 @@ export default function PriceInventoryTab({ product, up, refs, variations, setVa
719
717
  type="button"
720
718
  className="flex items-center gap-1.5 text-left font-medium"
721
719
  onClick={() => setExpanded(expanded === i ? null : i)}
722
- title="More fields (image, sale, shipping)"
720
+ title={t("products.more_fields")}
723
721
  >
724
722
  {expanded === i ? (
725
723
  <ChevronDown className="h-4 w-4 shrink-0 text-muted-foreground" />
@@ -739,7 +737,7 @@ export default function PriceInventoryTab({ product, up, refs, variations, setVa
739
737
  <Input
740
738
  type="number"
741
739
  placeholder="—"
742
- title={v.manage_stock === "yes" ? "Tracked quantity" : "Not tracked — type a quantity to track"}
740
+ title={v.manage_stock === "yes" ? t("products.tracked_quantity") : t("products.not_tracked")}
743
741
  value={v.manage_stock === "yes" ? v.stock_quantity ?? "" : ""}
744
742
  onChange={(e) =>
745
743
  setVariation(
@@ -764,7 +762,7 @@ export default function PriceInventoryTab({ product, up, refs, variations, setVa
764
762
  className="h-4 w-4 accent-foreground"
765
763
  checked={isDefault(v)}
766
764
  onChange={() => makeDefault(v)}
767
- aria-label={`Make ${variationLabel(v)} the default`}
765
+ aria-label={t("products.make_default", { name: variationLabel(v) })}
768
766
  />
769
767
  </td>
770
768
  <td className="p-2 text-center">
@@ -773,7 +771,7 @@ export default function PriceInventoryTab({ product, up, refs, variations, setVa
773
771
  onCheckedChange={(on) =>
774
772
  setVariation(i, { ...v, status: on ? "publish" : "private" })
775
773
  }
776
- aria-label={`Enable ${variationLabel(v)}`}
774
+ aria-label={t("products.enable_variant", { name: variationLabel(v) })}
777
775
  />
778
776
  </td>
779
777
  </tr>
@@ -806,9 +804,9 @@ export default function PriceInventoryTab({ product, up, refs, variations, setVa
806
804
  <ConfirmDialog
807
805
  open={confirmSingle}
808
806
  onOpenChange={setConfirmSingle}
809
- title="Switch to a single product?"
810
- description={`This removes ${variations.length} variant(s) and their stock and prices. The product keeps one price and one stock count.`}
811
- confirmLabel="Remove variants"
807
+ title={t("products.switch_single_title")}
808
+ description={t("products.switch_single_description", { count: variations.length })}
809
+ confirmLabel={t("products.remove_variants")}
812
810
  onConfirm={() => {
813
811
  up({ attributes: [], default_attributes: [] });
814
812
  setVariations([]);