@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
@@ -6,6 +6,7 @@ import { FolderTree, Loader2, Plus, Trash2 } from "lucide-react";
6
6
  import { toast } from "sonner";
7
7
 
8
8
  import { base44, call } from "../../lib/api";
9
+ import { t } from "../../i18n";
9
10
  import useAsync from "../../hooks/useAsync";
10
11
  import PageHeader from "../../components/PageHeader";
11
12
  import ConfirmDialog from "../../components/ConfirmDialog";
@@ -80,7 +81,7 @@ export default function Categories() {
80
81
  parent_id: row.parent_id,
81
82
  },
82
83
  });
83
- toast.success(row.id ? "Category saved" : "Category added");
84
+ toast.success(row.id ? t("categories.saved") : t("categories.added"));
84
85
  refetch();
85
86
  } catch {
86
87
  /* toast handled by call() */
@@ -93,7 +94,7 @@ export default function Categories() {
93
94
  setDeleting(true);
94
95
  try {
95
96
  await call("admin-products", "delete-term", { taxonomy: "category", id: confirmDelete.id });
96
- toast.success("Category deleted");
97
+ toast.success(t("categories.deleted"));
97
98
  setConfirmDelete(null);
98
99
  refetch();
99
100
  } catch {
@@ -115,7 +116,7 @@ export default function Categories() {
115
116
 
116
117
  return (
117
118
  <div className="space-y-4">
118
- <PageHeader title="Categories" description="Organize products into categories" />
119
+ <PageHeader title={t("routes.categories")} description={t("categories.description")} />
119
120
 
120
121
  <Card className="max-w-3xl">
121
122
  <CardContent className="space-y-2 py-4">
@@ -124,15 +125,15 @@ export default function Categories() {
124
125
  {!loading && !rows.length && (
125
126
  <div className="flex items-center gap-3 py-6 text-muted-foreground">
126
127
  <FolderTree className="h-5 w-5" />
127
- <span className="text-sm">No categories yet — add the first one below.</span>
128
+ <span className="text-sm">{t("categories.empty")}</span>
128
129
  </div>
129
130
  )}
130
131
 
131
132
  {rows.length > 0 && (
132
133
  <div className="grid grid-cols-[1fr_1fr_5rem_2.25rem] items-center gap-2 px-1 text-xs font-medium uppercase tracking-wide text-muted-foreground">
133
- <span>Name</span>
134
- <span>Slug</span>
135
- <span>Order</span>
134
+ <span>{t("common.name")}</span>
135
+ <span>{t("categories.slug")}</span>
136
+ <span>{t("categories.order")}</span>
136
137
  <span />
137
138
  </div>
138
139
  )}
@@ -141,14 +142,14 @@ export default function Categories() {
141
142
  <div key={row.id || `new-${i}`} className="grid grid-cols-[1fr_1fr_5rem_2.25rem] items-center gap-2">
142
143
  <Input
143
144
  autoFocus={!row.id && !row.name}
144
- placeholder="Category name"
145
+ placeholder={t("categories.name_placeholder")}
145
146
  value={row.name}
146
147
  onChange={(e) => setField(i, "name", e.target.value)}
147
148
  onBlur={() => commit(i)}
148
149
  onKeyDown={blurOnEnter}
149
150
  />
150
151
  <Input
151
- placeholder="auto from name"
152
+ placeholder={t("categories.slug_placeholder")}
152
153
  className="font-mono text-xs lowercase"
153
154
  value={row.slug}
154
155
  onChange={(e) => setField(i, "slug", e.target.value)}
@@ -187,7 +188,7 @@ export default function Categories() {
187
188
  ])
188
189
  }
189
190
  >
190
- <Plus className="mr-1 h-3 w-3" /> Add category
191
+ <Plus className="mr-1 h-3 w-3" /> {t("categories.add")}
191
192
  </Button>
192
193
  </CardContent>
193
194
  </Card>
@@ -195,9 +196,9 @@ export default function Categories() {
195
196
  <ConfirmDialog
196
197
  open={Boolean(confirmDelete)}
197
198
  onOpenChange={(o) => !o && setConfirmDelete(null)}
198
- title={`Delete "${confirmDelete?.name}"?`}
199
- description="Products in this category are not deleted; they simply lose the category. This cannot be undone."
200
- confirmLabel="Delete"
199
+ title={t("categories.confirm_delete_title", { name: confirmDelete?.name })}
200
+ description={t("categories.confirm_delete_description")}
201
+ confirmLabel={t("common.delete")}
201
202
  onConfirm={doDelete}
202
203
  loading={deleting}
203
204
  />
@@ -6,6 +6,7 @@ import { Skeleton } from "@/components/ui/skeleton";
6
6
  import { toast } from "sonner";
7
7
 
8
8
  import { base44, call } from "../../lib/api";
9
+ import { t } from "../../i18n";
9
10
  import useAsync from "../../hooks/useAsync";
10
11
  import { useAdminHref } from "../../context/BasePathContext";
11
12
  import PageHeader from "../../components/PageHeader";
@@ -107,7 +108,7 @@ export default function ProductEditor() {
107
108
  }, [id, isNew]);
108
109
 
109
110
  useEffect(() => {
110
- load().catch((e) => toast.error(e.message || "Failed to load product"));
111
+ load().catch((e) => toast.error(e.message || t("products.load_failed")));
111
112
  }, [load]);
112
113
 
113
114
  /** Patch the product object. */
@@ -123,7 +124,7 @@ export default function ProductEditor() {
123
124
 
124
125
  const save = async () => {
125
126
  if (!product.name?.trim()) {
126
- toast.error("Product name is required");
127
+ toast.error(t("products.name_required"));
127
128
  return;
128
129
  }
129
130
  setSaving(true);
@@ -131,7 +132,7 @@ export default function ProductEditor() {
131
132
  const payload = { product: { ...product, name: product.name.trim() }, variations };
132
133
  const data = await call("admin-products", "save", payload);
133
134
  const savedId = data?.product?.id || data?.id || id;
134
- toast.success(isNew ? "Product created" : "Product saved");
135
+ toast.success(isNew ? t("products.created") : t("products.saved"));
135
136
  if (isNew && savedId) navigate(href(`products/${savedId}`), { replace: true });
136
137
  else await load();
137
138
  } catch {
@@ -143,7 +144,7 @@ export default function ProductEditor() {
143
144
 
144
145
  const doDelete = async () => {
145
146
  await call("admin-products", "delete", { id });
146
- toast.success("Product deleted");
147
+ toast.success(t("products.deleted"));
147
148
  navigate(href("products"));
148
149
  };
149
150
 
@@ -170,7 +171,7 @@ export default function ProductEditor() {
170
171
  return (
171
172
  <div className="space-y-4">
172
173
  <PageHeader
173
- title={isNew ? "Add new product" : "Edit product"}
174
+ title={isNew ? t("products.add_new_product") : t("routes.products_edit")}
174
175
  backHref={href("products")}
175
176
  />
176
177
 
@@ -179,13 +180,13 @@ export default function ProductEditor() {
179
180
  <div className="min-w-0 space-y-4">
180
181
  <Input
181
182
  className="h-11 text-lg font-medium"
182
- placeholder="Product name"
183
+ placeholder={t("products.name_placeholder")}
183
184
  value={product.name}
184
185
  onChange={(e) => setName(e.target.value)}
185
186
  />
186
187
 
187
188
  <div className="flex items-center gap-2 text-sm text-muted-foreground">
188
- <span>Slug:</span>
189
+ <span>{t("products.slug_label")}</span>
189
190
  <Input
190
191
  className="h-7 w-72 text-xs"
191
192
  value={product.slug || ""}
@@ -198,12 +199,12 @@ export default function ProductEditor() {
198
199
 
199
200
  <Card>
200
201
  <CardHeader className="pb-2">
201
- <CardTitle className="text-base">Description</CardTitle>
202
+ <CardTitle className="text-base">{t("common.description")}</CardTitle>
202
203
  </CardHeader>
203
204
  <CardContent>
204
205
  <RichTextarea
205
206
  rows={8}
206
- placeholder="Full product description (HTML allowed)"
207
+ placeholder={t("products.description_placeholder")}
207
208
  value={product.description || ""}
208
209
  onChange={(v) => up({ description: v })}
209
210
  />
@@ -212,12 +213,12 @@ export default function ProductEditor() {
212
213
 
213
214
  <Card>
214
215
  <CardHeader className="pb-2">
215
- <CardTitle className="text-base">Short description</CardTitle>
216
+ <CardTitle className="text-base">{t("products.short_description")}</CardTitle>
216
217
  </CardHeader>
217
218
  <CardContent>
218
219
  <RichTextarea
219
220
  rows={4}
220
- placeholder="Short summary shown in listings"
221
+ placeholder={t("products.short_description_placeholder")}
221
222
  value={product.short_description || ""}
222
223
  onChange={(v) => up({ short_description: v })}
223
224
  />
@@ -246,19 +247,19 @@ export default function ProductEditor() {
246
247
 
247
248
  <Card>
248
249
  <CardHeader className="pb-2">
249
- <CardTitle className="text-base">Product image</CardTitle>
250
+ <CardTitle className="text-base">{t("products.product_image")}</CardTitle>
250
251
  </CardHeader>
251
252
  <CardContent>
252
- <MediaUploader value={primaryImage} onChange={setPrimaryImage} label="Set product image" />
253
+ <MediaUploader value={primaryImage} onChange={setPrimaryImage} label={t("products.set_product_image")} />
253
254
  </CardContent>
254
255
  </Card>
255
256
 
256
257
  <Card>
257
258
  <CardHeader className="pb-2">
258
- <CardTitle className="text-base">Product gallery</CardTitle>
259
+ <CardTitle className="text-base">{t("products.product_gallery")}</CardTitle>
259
260
  </CardHeader>
260
261
  <CardContent>
261
- <MediaUploader multiple value={gallery} onChange={setGallery} label="Add gallery images" />
262
+ <MediaUploader multiple value={gallery} onChange={setGallery} label={t("products.add_gallery_images")} />
262
263
  </CardContent>
263
264
  </Card>
264
265
 
@@ -21,6 +21,7 @@ import { Copy, ImageIcon, Package, Pencil, Plus, Star, Trash2, Zap } from "lucid
21
21
  import { toast } from "sonner";
22
22
 
23
23
  import { base44, call } from "../../lib/api";
24
+ import { t } from "../../i18n";
24
25
  import usePagedList from "../../hooks/usePagedList";
25
26
  import useAsync from "../../hooks/useAsync";
26
27
  import useDebounce from "../../hooks/useDebounce";
@@ -59,7 +60,7 @@ function QuickEdit({ product, open, onOpenChange, onSaved }) {
59
60
  setSaving(true);
60
61
  try {
61
62
  await call("admin-products", "save", { product: { id: product.id, ...form } });
62
- toast.success("Product updated");
63
+ toast.success(t("products.updated"));
63
64
  onOpenChange(false);
64
65
  onSaved();
65
66
  } catch {
@@ -74,20 +75,20 @@ function QuickEdit({ product, open, onOpenChange, onSaved }) {
74
75
  <DialogContent className="sm:max-w-sm">
75
76
  <DialogHeader>
76
77
  <DialogTitle className="flex items-center gap-2">
77
- <Zap className="h-4 w-4" /> Quick edit
78
+ <Zap className="h-4 w-4" /> {t("products.quick_edit")}
78
79
  </DialogTitle>
79
80
  </DialogHeader>
80
81
  <div className="space-y-3">
81
82
  <div className="space-y-1.5">
82
- <Label>Regular price</Label>
83
+ <Label>{t("products.regular_price")}</Label>
83
84
  <MoneyInput value={form.regular_price} onChange={(v) => setForm((f) => ({ ...f, regular_price: v }))} />
84
85
  </div>
85
86
  <div className="space-y-1.5">
86
- <Label>Sale price</Label>
87
+ <Label>{t("products.sale_price")}</Label>
87
88
  <MoneyInput value={form.sale_price} onChange={(v) => setForm((f) => ({ ...f, sale_price: v }))} />
88
89
  </div>
89
90
  <div className="space-y-1.5">
90
- <Label>Stock quantity</Label>
91
+ <Label>{t("products.stock_quantity")}</Label>
91
92
  <Input
92
93
  type="number"
93
94
  value={form.stock_quantity ?? ""}
@@ -97,7 +98,7 @@ function QuickEdit({ product, open, onOpenChange, onSaved }) {
97
98
  />
98
99
  </div>
99
100
  <div className="space-y-1.5">
100
- <Label>Status</Label>
101
+ <Label>{t("common.status")}</Label>
101
102
  <Select value={form.status} onValueChange={(v) => setForm((f) => ({ ...f, status: v }))}>
102
103
  <SelectTrigger><SelectValue /></SelectTrigger>
103
104
  <SelectContent>
@@ -109,7 +110,7 @@ function QuickEdit({ product, open, onOpenChange, onSaved }) {
109
110
  </div>
110
111
  </div>
111
112
  <DialogFooter>
112
- <Button onClick={save} disabled={saving}>Save</Button>
113
+ <Button onClick={save} disabled={saving}>{t("common.save")}</Button>
113
114
  </DialogFooter>
114
115
  </DialogContent>
115
116
  </Dialog>
@@ -168,7 +169,7 @@ export default function ProductsList() {
168
169
  const duplicate = async (row) => {
169
170
  const data = await call("admin-products", "duplicate", { id: row.id });
170
171
  const newId = data?.product?.id || data?.id;
171
- toast.success("Product duplicated");
172
+ toast.success(t("products.duplicated"));
172
173
  if (newId) navigate(href(`products/${newId}`));
173
174
  else list.refetch();
174
175
  };
@@ -178,11 +179,11 @@ export default function ProductsList() {
178
179
  try {
179
180
  if (confirmDelete === "bulk") {
180
181
  for (const id of selected) await call("admin-products", "delete", { id });
181
- toast.success(`${selected.length} product(s) deleted`);
182
+ toast.success(t("products.n_deleted", { count: selected.length }));
182
183
  setSelected([]);
183
184
  } else {
184
185
  await call("admin-products", "delete", { id: confirmDelete.id });
185
- toast.success("Product deleted");
186
+ toast.success(t("products.deleted"));
186
187
  }
187
188
  setConfirmDelete(null);
188
189
  list.refetch();
@@ -209,7 +210,7 @@ export default function ProductsList() {
209
210
  },
210
211
  {
211
212
  key: "name",
212
- label: "Name",
213
+ label: t("common.name"),
213
214
  sortable: true,
214
215
  render: (row) => (
215
216
  <div>
@@ -218,16 +219,16 @@ export default function ProductsList() {
218
219
  className="font-medium hover:underline"
219
220
  onClick={() => navigate(href(`products/${row.id}`))}
220
221
  >
221
- {row.name || "(no title)"}
222
+ {row.name || t("products.no_title")}
222
223
  </button>
223
- {row.sku && <div className="text-xs text-muted-foreground">SKU: {row.sku}</div>}
224
+ {row.sku && <div className="text-xs text-muted-foreground">{t("products.sku_line", { sku: row.sku })}</div>}
224
225
  </div>
225
226
  ),
226
227
  },
227
- { key: "sku", label: "SKU", className: "hidden md:table-cell", render: (row) => row.sku || "—" },
228
+ { key: "sku", label: t("products.sku"), className: "hidden md:table-cell", render: (row) => row.sku || "—" },
228
229
  {
229
230
  key: "stock_status",
230
- label: "Stock",
231
+ label: t("products.stock"),
231
232
  render: (row) => (
232
233
  <div className="flex items-center gap-1.5">
233
234
  <StatusBadge status={row.stock_status} map={STOCK_STATUSES} />
@@ -239,7 +240,7 @@ export default function ProductsList() {
239
240
  },
240
241
  {
241
242
  key: "price",
242
- label: "Price",
243
+ label: t("products.price"),
243
244
  sortable: true,
244
245
  render: (row) =>
245
246
  row.on_sale && row.sale_price != null ? (
@@ -255,7 +256,7 @@ export default function ProductsList() {
255
256
  },
256
257
  {
257
258
  key: "categories",
258
- label: "Categories",
259
+ label: t("routes.categories"),
259
260
  className: "hidden lg:table-cell",
260
261
  render: (row) => {
261
262
  const names = (row.category_ids || []).map(categoryName).filter(Boolean);
@@ -267,7 +268,7 @@ export default function ProductsList() {
267
268
  label: "★",
268
269
  className: "w-10",
269
270
  render: (row) => (
270
- <button type="button" onClick={() => toggleFeatured(row)} title="Toggle featured">
271
+ <button type="button" onClick={() => toggleFeatured(row)} title={t("products.toggle_featured")}>
271
272
  <Star
272
273
  className={`h-4 w-4 ${row.featured ? "fill-amber-400 text-amber-400" : "text-muted-foreground/40"}`}
273
274
  />
@@ -276,14 +277,14 @@ export default function ProductsList() {
276
277
  },
277
278
  {
278
279
  key: "created_date",
279
- label: "Date",
280
+ label: t("products.date"),
280
281
  sortable: true,
281
282
  className: "hidden md:table-cell",
282
283
  render: (row) => formatDate(row.created_date),
283
284
  },
284
285
  {
285
286
  key: "status",
286
- label: "Status",
287
+ label: t("common.status"),
287
288
  render: (row) => <StatusBadge status={row.status} map={PRODUCT_STATUSES} />,
288
289
  },
289
290
  ];
@@ -291,25 +292,25 @@ export default function ProductsList() {
291
292
  return (
292
293
  <div className="space-y-4">
293
294
  <PageHeader
294
- title="Products"
295
- description="Manage your catalog"
295
+ title={t("routes.products")}
296
+ description={t("products.manage_catalog")}
296
297
  actions={
297
298
  <Button onClick={() => navigate(href("products/new"))}>
298
- <Plus className="mr-2 h-4 w-4" /> Add new
299
+ <Plus className="mr-2 h-4 w-4" /> {t("products.add_new")}
299
300
  </Button>
300
301
  }
301
302
  />
302
303
 
303
304
  <div className="flex flex-wrap items-end gap-2">
304
305
  <Input
305
- placeholder="Search products"
306
+ placeholder={t("products.search_placeholder")}
306
307
  value={q}
307
308
  onChange={(e) => setQ(e.target.value)}
308
309
  className="w-64"
309
310
  />
310
311
  <SearchSelect
311
312
  className="w-56"
312
- placeholder="All categories"
313
+ placeholder={t("products.all_categories")}
313
314
  value={category}
314
315
  onChange={setCategory}
315
316
  search={async (query) =>
@@ -322,7 +323,7 @@ export default function ProductsList() {
322
323
  <Select value={stock} onValueChange={setStock}>
323
324
  <SelectTrigger className="w-40"><SelectValue /></SelectTrigger>
324
325
  <SelectContent>
325
- <SelectItem value={ALL}>All stock</SelectItem>
326
+ <SelectItem value={ALL}>{t("products.all_stock")}</SelectItem>
326
327
  {STOCK_STATUSES.map((s) => (
327
328
  <SelectItem key={s.value} value={s.value}>{s.label}</SelectItem>
328
329
  ))}
@@ -330,7 +331,7 @@ export default function ProductsList() {
330
331
  </Select>
331
332
  {selected.length > 0 && (
332
333
  <Button variant="destructive" size="sm" onClick={() => setConfirmDelete("bulk")}>
333
- <Trash2 className="mr-2 h-4 w-4" /> Delete ({selected.length})
334
+ <Trash2 className="mr-2 h-4 w-4" /> {t("products.delete_count", { count: selected.length })}
334
335
  </Button>
335
336
  )}
336
337
  </div>
@@ -346,15 +347,15 @@ export default function ProductsList() {
346
347
  onSelectChange={setSelected}
347
348
  pagination={{ page: list.page, hasNext: list.hasNext, onNext: list.next, onPrev: list.prev }}
348
349
  rowActions={(row) => [
349
- { label: "Edit", icon: Pencil, onClick: () => navigate(href(`products/${row.id}`)) },
350
- { label: "Quick edit", icon: Zap, onClick: () => setQuickEdit(row) },
351
- { label: "Duplicate", icon: Copy, onClick: () => duplicate(row) },
352
- { label: "Delete", icon: Trash2, destructive: true, onClick: () => setConfirmDelete(row) },
350
+ { label: t("common.edit"), icon: Pencil, onClick: () => navigate(href(`products/${row.id}`)) },
351
+ { label: t("products.quick_edit"), icon: Zap, onClick: () => setQuickEdit(row) },
352
+ { label: t("products.duplicate"), icon: Copy, onClick: () => duplicate(row) },
353
+ { label: t("common.delete"), icon: Trash2, destructive: true, onClick: () => setConfirmDelete(row) },
353
354
  ]}
354
355
  empty={{
355
356
  icon: Package,
356
- title: "No products found",
357
- description: usingSearch ? "Try a different search or filter." : "Create your first product to get started.",
357
+ title: t("products.empty_title"),
358
+ description: usingSearch ? t("products.empty_search") : t("products.empty_create"),
358
359
  }}
359
360
  />
360
361
 
@@ -368,9 +369,13 @@ export default function ProductsList() {
368
369
  <ConfirmDialog
369
370
  open={Boolean(confirmDelete)}
370
371
  onOpenChange={(o) => !o && setConfirmDelete(null)}
371
- title={confirmDelete === "bulk" ? `Delete ${selected.length} product(s)?` : `Delete "${confirmDelete?.name}"?`}
372
- description="This permanently deletes the product and its variations."
373
- confirmLabel="Delete"
372
+ title={
373
+ confirmDelete === "bulk"
374
+ ? t("products.confirm_delete_bulk", { count: selected.length })
375
+ : t("products.confirm_delete_one", { name: confirmDelete?.name })
376
+ }
377
+ description={t("products.confirm_delete_description")}
378
+ confirmLabel={t("common.delete")}
374
379
  onConfirm={doDelete}
375
380
  loading={deleting}
376
381
  />