@base44/app-plugin-commerce 0.7.0 → 0.8.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 (81) 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 +18 -30
  9. package/skills/commerce/install/02-storefront.md +23 -9
  10. package/skills/commerce/references/admin-localization.md +95 -0
  11. package/skills/commerce/references/reviews.md +7 -6
  12. package/src/commerce/admin/README.md +36 -15
  13. package/src/commerce/admin/bot/StoreAdminBot.jsx +12 -11
  14. package/src/commerce/admin/components/AddressForm.jsx +13 -12
  15. package/src/commerce/admin/components/ConfirmDialog.jsx +5 -4
  16. package/src/commerce/admin/components/CountrySelect.jsx +12 -8
  17. package/src/commerce/admin/components/DataTable.jsx +3 -2
  18. package/src/commerce/admin/components/DateRangePicker.jsx +11 -10
  19. package/src/commerce/admin/components/EmptyState.jsx +2 -1
  20. package/src/commerce/admin/components/MediaUploader.jsx +3 -2
  21. package/src/commerce/admin/components/MetaDataEditor.jsx +4 -3
  22. package/src/commerce/admin/components/SearchSelect.jsx +4 -3
  23. package/src/commerce/admin/context/SettingsContext.jsx +12 -25
  24. package/src/commerce/admin/hooks/useMoney.js +3 -2
  25. package/src/commerce/admin/i18n/index.js +54 -0
  26. package/src/commerce/admin/i18n/locales/de.js +896 -0
  27. package/src/commerce/admin/i18n/locales/en.js +900 -0
  28. package/src/commerce/admin/i18n/locales/es.js +896 -0
  29. package/src/commerce/admin/i18n/locales/fr.js +896 -0
  30. package/src/commerce/admin/i18n/locales/ja.js +896 -0
  31. package/src/commerce/admin/i18n/locales/pt.js +896 -0
  32. package/src/commerce/admin/index.jsx +6 -30
  33. package/src/commerce/admin/layout/AccessDenied.jsx +8 -9
  34. package/src/commerce/admin/layout/AuthGuard.jsx +2 -1
  35. package/src/commerce/admin/layout/Sidebar.jsx +29 -18
  36. package/src/commerce/admin/layout/Topbar.jsx +4 -3
  37. package/src/commerce/admin/lib/api.js +3 -2
  38. package/src/commerce/admin/lib/constants.js +44 -43
  39. package/src/commerce/admin/lib/format.js +4 -3
  40. package/src/commerce/admin/lib/paths.js +2 -3
  41. package/src/commerce/admin/pages/Dashboard.jsx +25 -24
  42. package/src/commerce/admin/pages/coupons/CouponEditor.jsx +55 -57
  43. package/src/commerce/admin/pages/coupons/CouponsList.jsx +22 -21
  44. package/src/commerce/admin/pages/customers/CustomerEditor.jsx +31 -30
  45. package/src/commerce/admin/pages/customers/CustomersList.jsx +19 -18
  46. package/src/commerce/admin/pages/orders/OrderEditor.jsx +75 -71
  47. package/src/commerce/admin/pages/orders/OrdersList.jsx +16 -15
  48. package/src/commerce/admin/pages/orders/components/AddProductDialog.jsx +12 -11
  49. package/src/commerce/admin/pages/orders/components/DownloadPermissionsPanel.jsx +14 -13
  50. package/src/commerce/admin/pages/orders/components/LineItemsTable.jsx +12 -11
  51. package/src/commerce/admin/pages/orders/components/OrderNotesPanel.jsx +11 -10
  52. package/src/commerce/admin/pages/orders/components/PaymentPanel.jsx +22 -22
  53. package/src/commerce/admin/pages/orders/components/RefundPanel.jsx +18 -17
  54. package/src/commerce/admin/pages/orders/components/TotalsBox.jsx +18 -17
  55. package/src/commerce/admin/pages/products/Categories.jsx +14 -13
  56. package/src/commerce/admin/pages/products/ProductEditor.jsx +16 -15
  57. package/src/commerce/admin/pages/products/ProductsList.jsx +41 -36
  58. package/src/commerce/admin/pages/products/Reviews.jsx +54 -46
  59. package/src/commerce/admin/pages/products/components/AttributesSection.jsx +45 -39
  60. package/src/commerce/admin/pages/products/components/ProductDataPanel.jsx +8 -7
  61. package/src/commerce/admin/pages/products/components/PublishBox.jsx +10 -9
  62. package/src/commerce/admin/pages/products/components/TaxonomyPanel.jsx +18 -17
  63. package/src/commerce/admin/pages/products/components/tabs/DownloadsTab.jsx +11 -10
  64. package/src/commerce/admin/pages/products/components/tabs/LinkedTab.jsx +9 -10
  65. package/src/commerce/admin/pages/products/components/tabs/ModifiersTab.jsx +3 -9
  66. package/src/commerce/admin/pages/products/components/tabs/PriceInventoryTab.jsx +76 -78
  67. package/src/commerce/admin/pages/reports/Reports.jsx +33 -32
  68. package/src/commerce/admin/pages/settings/EmailsSettings.jsx +36 -39
  69. package/src/commerce/admin/pages/settings/GeneralSettings.jsx +13 -14
  70. package/src/commerce/admin/pages/settings/InventorySettings.jsx +12 -13
  71. package/src/commerce/admin/pages/settings/LocationEditor.jsx +48 -47
  72. package/src/commerce/admin/pages/settings/PaymentsSettings.jsx +32 -33
  73. package/src/commerce/admin/pages/settings/SettingsLayout.jsx +13 -12
  74. package/src/commerce/admin/pages/settings/ShippingTaxSettings.jsx +31 -31
  75. package/src/commerce/admin/pages/settings/useGroupForm.jsx +4 -3
  76. package/src/commerce/admin/pages/status/WebhookEditor.jsx +34 -31
  77. package/src/commerce/admin/pages/status/Webhooks.jsx +11 -10
  78. package/src/commerce/admin/routes.jsx +31 -43
  79. package/src/commerce/storefront/pickers.jsx +19 -5
  80. package/src/commerce/storefront/useCheckout.jsx +28 -3
  81. package/src/commerce/utils/storefront.js +49 -2
@@ -20,6 +20,7 @@ import {
20
20
  import { Loader2, Wand2, X } from "lucide-react";
21
21
 
22
22
  import { call, base44 } from "../../lib/api";
23
+ import { t } from "../../i18n";
23
24
  import { COUPON_TYPES } from "../../lib/constants";
24
25
  import useMoney from "../../hooks/useMoney";
25
26
  import { useAdminHref } from "../../context/BasePathContext";
@@ -82,7 +83,7 @@ function EmailChips({ value = [], onChange }) {
82
83
  }
83
84
  }}
84
85
  onBlur={add}
85
- placeholder="email@example.com or *@example.com — press Enter"
86
+ placeholder={t("coupons.email_chips_placeholder")}
86
87
  />
87
88
  {value.length > 0 && (
88
89
  <div className="mt-2 flex flex-wrap gap-1">
@@ -104,13 +105,13 @@ function EmailChips({ value = [], onChange }) {
104
105
  );
105
106
  }
106
107
 
107
- function NumberField({ value, onChange, placeholder = "Unlimited" }) {
108
+ function NumberField({ value, onChange, placeholder }) {
108
109
  return (
109
110
  <Input
110
111
  type="number"
111
112
  min="0"
112
113
  value={value ?? ""}
113
- placeholder={placeholder}
114
+ placeholder={placeholder ?? t("coupons.unlimited")}
114
115
  onChange={(e) => onChange(e.target.value === "" ? null : Number(e.target.value))}
115
116
  />
116
117
  );
@@ -189,11 +190,11 @@ export default function CouponEditor() {
189
190
 
190
191
  const save = async () => {
191
192
  if (!coupon.code?.trim()) {
192
- toast.error("A coupon code is required");
193
+ toast.error(t("coupons.code_required"));
193
194
  return;
194
195
  }
195
196
  if (coupon.amount === null || coupon.amount === undefined) {
196
- toast.error("A discount amount is required");
197
+ toast.error(t("coupons.amount_required"));
197
198
  return;
198
199
  }
199
200
  setSaving(true);
@@ -207,7 +208,7 @@ export default function CouponEditor() {
207
208
  : null,
208
209
  };
209
210
  const saved = await call("admin-coupons", "save", { coupon: payload });
210
- toast.success("Coupon saved");
211
+ toast.success(t("coupons.saved"));
211
212
  if (isNew && saved?.id) navigate(href(`coupons/${saved.id}`), { replace: true });
212
213
  else if (saved) setCoupon((c) => ({ ...c, ...saved }));
213
214
  } catch {
@@ -221,7 +222,7 @@ export default function CouponEditor() {
221
222
  setDeleting(true);
222
223
  try {
223
224
  await call("admin-coupons", "delete", { id });
224
- toast.success("Coupon deleted");
225
+ toast.success(t("coupons.deleted"));
225
226
  navigate(href("coupons"));
226
227
  } catch {
227
228
  setDeleting(false);
@@ -241,7 +242,7 @@ export default function CouponEditor() {
241
242
  return (
242
243
  <div>
243
244
  <PageHeader
244
- title={isNew ? "Add coupon" : `Coupon: ${coupon.code}`}
245
+ title={isNew ? t("routes.coupons_new") : t("coupons.title_with_code", { code: coupon.code })}
245
246
  backHref={href("coupons")}
246
247
  />
247
248
 
@@ -251,12 +252,12 @@ export default function CouponEditor() {
251
252
  <Card>
252
253
  <CardContent className="space-y-4 pt-6">
253
254
  <div className="grid gap-1.5">
254
- <Label>Coupon code</Label>
255
+ <Label>{t("coupons.coupon_code")}</Label>
255
256
  <div className="flex gap-2">
256
257
  <Input
257
258
  value={coupon.code}
258
259
  onChange={(e) => set("code")(e.target.value)}
259
- placeholder="e.g. welcome10"
260
+ placeholder={t("coupons.code_placeholder")}
260
261
  className="font-mono lowercase"
261
262
  />
262
263
  <Button
@@ -264,12 +265,12 @@ export default function CouponEditor() {
264
265
  variant="outline"
265
266
  onClick={() => set("code")(randomCode())}
266
267
  >
267
- <Wand2 className="mr-1 h-4 w-4" /> Generate
268
+ <Wand2 className="mr-1 h-4 w-4" /> {t("coupons.generate")}
268
269
  </Button>
269
270
  </div>
270
271
  </div>
271
272
  <div className="grid gap-1.5">
272
- <Label>Description (optional)</Label>
273
+ <Label>{t("coupons.description_optional")}</Label>
273
274
  <Textarea
274
275
  rows={2}
275
276
  value={coupon.description || ""}
@@ -282,11 +283,11 @@ export default function CouponEditor() {
282
283
  {/* Coupon data: stacked cards, like the product editor's sections */}
283
284
  <Card>
284
285
  <CardHeader className="pb-2">
285
- <CardTitle className="text-base">General</CardTitle>
286
+ <CardTitle className="text-base">{t("coupons.general")}</CardTitle>
286
287
  </CardHeader>
287
288
  <CardContent className="space-y-4">
288
289
  <div className="grid gap-1.5 sm:max-w-sm">
289
- <Label>Discount type</Label>
290
+ <Label>{t("coupons.discount_type")}</Label>
290
291
  <Select value={coupon.discount_type} onValueChange={set("discount_type")}>
291
292
  <SelectTrigger>
292
293
  <SelectValue />
@@ -301,7 +302,7 @@ export default function CouponEditor() {
301
302
  </Select>
302
303
  </div>
303
304
  <div className="grid gap-1.5 sm:max-w-sm">
304
- <Label>Coupon amount {isPercent ? "(%)" : ""}</Label>
305
+ <Label>{isPercent ? t("coupons.amount_label_percent") : t("coupons.amount_label")}</Label>
305
306
  {isPercent ? (
306
307
  <div className="relative">
307
308
  <Input
@@ -330,14 +331,14 @@ export default function CouponEditor() {
330
331
  className="mt-0.5"
331
332
  />
332
333
  <span>
333
- Allow free shipping
334
+ {t("coupons.free_shipping")}
334
335
  <span className="block text-xs text-muted-foreground">
335
- Grants access to free-shipping methods that require a coupon.
336
+ {t("coupons.free_shipping_hint")}
336
337
  </span>
337
338
  </span>
338
339
  </label>
339
340
  <div className="grid gap-1.5 sm:max-w-sm">
340
- <Label>Coupon expiry date</Label>
341
+ <Label>{t("coupons.coupon_expiry_date")}</Label>
341
342
  <Input
342
343
  type="date"
343
344
  value={coupon.date_expires ? coupon.date_expires.slice(0, 10) : ""}
@@ -349,24 +350,24 @@ export default function CouponEditor() {
349
350
 
350
351
  <Card>
351
352
  <CardHeader className="pb-2">
352
- <CardTitle className="text-base">Usage restriction</CardTitle>
353
+ <CardTitle className="text-base">{t("coupons.usage_restriction")}</CardTitle>
353
354
  </CardHeader>
354
355
  <CardContent className="space-y-4">
355
356
  <div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
356
357
  <div className="grid gap-1.5">
357
- <Label>Minimum spend ({money.code})</Label>
358
+ <Label>{t("coupons.minimum_spend", { code: money.code })}</Label>
358
359
  <MoneyInput
359
360
  value={coupon.minimum_amount}
360
361
  onChange={set("minimum_amount")}
361
- placeholder="No minimum"
362
+ placeholder={t("coupons.no_minimum")}
362
363
  />
363
364
  </div>
364
365
  <div className="grid gap-1.5">
365
- <Label>Maximum spend ({money.code})</Label>
366
+ <Label>{t("coupons.maximum_spend", { code: money.code })}</Label>
366
367
  <MoneyInput
367
368
  value={coupon.maximum_amount}
368
369
  onChange={set("maximum_amount")}
369
- placeholder="No maximum"
370
+ placeholder={t("coupons.no_maximum")}
370
371
  />
371
372
  </div>
372
373
  </div>
@@ -377,9 +378,9 @@ export default function CouponEditor() {
377
378
  className="mt-0.5"
378
379
  />
379
380
  <span>
380
- Individual use only
381
+ {t("coupons.individual_use")}
381
382
  <span className="block text-xs text-muted-foreground">
382
- Cannot be used in conjunction with other coupons.
383
+ {t("coupons.individual_use_hint")}
383
384
  </span>
384
385
  </span>
385
386
  </label>
@@ -390,21 +391,21 @@ export default function CouponEditor() {
390
391
  className="mt-0.5"
391
392
  />
392
393
  <span>
393
- Exclude sale items
394
+ {t("coupons.exclude_sale_items")}
394
395
  <span className="block text-xs text-muted-foreground">
395
- The coupon will not apply to items on sale.
396
+ {t("coupons.exclude_sale_items_hint")}
396
397
  </span>
397
398
  </span>
398
399
  </label>
399
400
  <Separator />
400
401
  <div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
401
402
  <div className="grid gap-1.5">
402
- <Label>Products</Label>
403
+ <Label>{t("routes.products")}</Label>
403
404
  <SearchSelect
404
405
  multiple
405
406
  search={searchProducts}
406
407
  value={productOpts.include}
407
- placeholder="Any product"
408
+ placeholder={t("coupons.any_product")}
408
409
  onChange={(opts) => {
409
410
  setProductOpts((o) => ({ ...o, include: opts }));
410
411
  set("product_ids")(opts.map((o) => o.value));
@@ -412,12 +413,12 @@ export default function CouponEditor() {
412
413
  />
413
414
  </div>
414
415
  <div className="grid gap-1.5">
415
- <Label>Exclude products</Label>
416
+ <Label>{t("coupons.exclude_products")}</Label>
416
417
  <SearchSelect
417
418
  multiple
418
419
  search={searchProducts}
419
420
  value={productOpts.exclude}
420
- placeholder="No exclusions"
421
+ placeholder={t("coupons.no_exclusions")}
421
422
  onChange={(opts) => {
422
423
  setProductOpts((o) => ({ ...o, exclude: opts }));
423
424
  set("excluded_product_ids")(opts.map((o) => o.value));
@@ -425,22 +426,22 @@ export default function CouponEditor() {
425
426
  />
426
427
  </div>
427
428
  <div className="grid gap-1.5">
428
- <Label>Product categories</Label>
429
+ <Label>{t("coupons.product_categories")}</Label>
429
430
  <SearchSelect
430
431
  multiple
431
432
  search={searchCategories}
432
433
  value={categoryOpts(coupon.product_category_ids)}
433
- placeholder="Any category"
434
+ placeholder={t("coupons.any_category")}
434
435
  onChange={(opts) => set("product_category_ids")(opts.map((o) => o.value))}
435
436
  />
436
437
  </div>
437
438
  <div className="grid gap-1.5">
438
- <Label>Exclude categories</Label>
439
+ <Label>{t("coupons.exclude_categories")}</Label>
439
440
  <SearchSelect
440
441
  multiple
441
442
  search={searchCategories}
442
443
  value={categoryOpts(coupon.excluded_product_category_ids)}
443
- placeholder="No exclusions"
444
+ placeholder={t("coupons.no_exclusions")}
444
445
  onChange={(opts) =>
445
446
  set("excluded_product_category_ids")(opts.map((o) => o.value))
446
447
  }
@@ -448,14 +449,13 @@ export default function CouponEditor() {
448
449
  </div>
449
450
  </div>
450
451
  <div className="grid gap-1.5">
451
- <Label>Allowed emails</Label>
452
+ <Label>{t("coupons.allowed_emails")}</Label>
452
453
  <EmailChips
453
454
  value={coupon.email_restrictions || []}
454
455
  onChange={set("email_restrictions")}
455
456
  />
456
457
  <p className="text-xs text-muted-foreground">
457
- Only these billing emails can use the coupon. Wildcards like{" "}
458
- <code>*@example.com</code> are allowed.
458
+ {t("coupons.allowed_emails_hint")}
459
459
  </p>
460
460
  </div>
461
461
  </CardContent>
@@ -463,36 +463,35 @@ export default function CouponEditor() {
463
463
 
464
464
  <Card>
465
465
  <CardHeader className="pb-2">
466
- <CardTitle className="text-base">Usage limits</CardTitle>
466
+ <CardTitle className="text-base">{t("coupons.usage_limits")}</CardTitle>
467
467
  </CardHeader>
468
468
  <CardContent className="max-w-sm space-y-4">
469
469
  <div className="grid gap-1.5">
470
- <Label>Usage limit per coupon</Label>
470
+ <Label>{t("coupons.usage_limit_per_coupon")}</Label>
471
471
  <NumberField value={coupon.usage_limit} onChange={set("usage_limit")} />
472
472
  <p className="text-xs text-muted-foreground">
473
- How many times the coupon can be used in total.
473
+ {t("coupons.usage_limit_per_coupon_hint")}
474
474
  </p>
475
475
  </div>
476
476
  <div className="grid gap-1.5">
477
- <Label>Limit usage to X items</Label>
477
+ <Label>{t("coupons.limit_usage_items")}</Label>
478
478
  <NumberField
479
479
  value={coupon.limit_usage_to_x_items}
480
480
  onChange={set("limit_usage_to_x_items")}
481
- placeholder="Apply to all qualifying items"
481
+ placeholder={t("coupons.apply_all_items")}
482
482
  />
483
483
  <p className="text-xs text-muted-foreground">
484
- Maximum number of individual items the coupon applies to (product
485
- discounts).
484
+ {t("coupons.limit_usage_items_hint")}
486
485
  </p>
487
486
  </div>
488
487
  <div className="grid gap-1.5">
489
- <Label>Usage limit per user</Label>
488
+ <Label>{t("coupons.usage_limit_per_user")}</Label>
490
489
  <NumberField
491
490
  value={coupon.usage_limit_per_user}
492
491
  onChange={set("usage_limit_per_user")}
493
492
  />
494
493
  <p className="text-xs text-muted-foreground">
495
- How many times a single customer (by billing email) can use it.
494
+ {t("coupons.usage_limit_per_user_hint")}
496
495
  </p>
497
496
  </div>
498
497
  </CardContent>
@@ -503,11 +502,11 @@ export default function CouponEditor() {
503
502
  <div className="space-y-4">
504
503
  <Card>
505
504
  <CardHeader className="pb-3">
506
- <CardTitle className="text-base">Publish</CardTitle>
505
+ <CardTitle className="text-base">{t("coupons.publish")}</CardTitle>
507
506
  </CardHeader>
508
507
  <CardContent className="space-y-3">
509
508
  <label className="flex items-center justify-between text-sm font-medium">
510
- Enabled
509
+ {t("coupons.enabled")}
511
510
  <Switch
512
511
  checked={coupon.enabled !== false}
513
512
  onCheckedChange={(v) => set("enabled")(!!v)}
@@ -515,18 +514,17 @@ export default function CouponEditor() {
515
514
  </label>
516
515
  {coupon.enabled === false && (
517
516
  <p className="text-xs text-muted-foreground">
518
- Disabled — customers can't apply this coupon, and carts holding it drop it.
517
+ {t("coupons.disabled_hint")}
519
518
  </p>
520
519
  )}
521
520
  {!isNew && (
522
521
  <p className="text-sm text-muted-foreground">
523
- Used: <span className="font-medium text-foreground">{coupon.usage_count ?? 0}</span>{" "}
524
- time{(coupon.usage_count ?? 0) === 1 ? "" : "s"}
522
+ {t("coupons.used_times", { count: coupon.usage_count ?? 0 })}
525
523
  </p>
526
524
  )}
527
525
  <Button className="w-full" onClick={save} disabled={saving}>
528
526
  {saving && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
529
- {isNew ? "Create coupon" : "Save coupon"}
527
+ {isNew ? t("coupons.create") : t("coupons.save")}
530
528
  </Button>
531
529
  {!isNew && (
532
530
  <Button
@@ -534,7 +532,7 @@ export default function CouponEditor() {
534
532
  className="w-full text-destructive hover:text-destructive"
535
533
  onClick={() => setConfirmDelete(true)}
536
534
  >
537
- Delete
535
+ {t("common.delete")}
538
536
  </Button>
539
537
  )}
540
538
  </CardContent>
@@ -545,9 +543,9 @@ export default function CouponEditor() {
545
543
  <ConfirmDialog
546
544
  open={confirmDelete}
547
545
  onOpenChange={setConfirmDelete}
548
- title="Delete this coupon?"
549
- description="Customers will no longer be able to use it. This cannot be undone."
550
- confirmLabel="Delete"
546
+ title={t("coupons.confirm_delete_one")}
547
+ description={t("coupons.confirm_delete_description")}
548
+ confirmLabel={t("common.delete")}
551
549
  loading={deleting}
552
550
  onConfirm={doDelete}
553
551
  />
@@ -6,6 +6,7 @@ import { Input } from "@/components/ui/input";
6
6
  import { Plus, Search, TicketPercent, Trash2, Pencil } from "lucide-react";
7
7
 
8
8
  import { call, base44 } from "../../lib/api";
9
+ import { t } from "../../i18n";
9
10
  import { COUPON_TYPES } from "../../lib/constants";
10
11
  import { formatDate, truncate } from "../../lib/format";
11
12
  import usePagedList from "../../hooks/usePagedList";
@@ -52,11 +53,11 @@ export default function CouponsList() {
52
53
  try {
53
54
  if (pendingDelete === "bulk") {
54
55
  await call("admin-coupons", "batch", { delete: selected });
55
- toast.success(`${selected.length} coupon(s) deleted`);
56
+ toast.success(t("coupons.n_deleted", { count: selected.length }));
56
57
  setSelected([]);
57
58
  } else {
58
59
  await call("admin-coupons", "delete", { id: pendingDelete.id });
59
- toast.success("Coupon deleted");
60
+ toast.success(t("coupons.deleted"));
60
61
  }
61
62
  setPendingDelete(null);
62
63
  list.refetch();
@@ -70,7 +71,7 @@ export default function CouponsList() {
70
71
  const columns = [
71
72
  {
72
73
  key: "code",
73
- label: "Code",
74
+ label: t("coupons.code"),
74
75
  sortable: !debouncedQuery,
75
76
  render: (c) => (
76
77
  <span className="inline-flex items-center gap-2">
@@ -83,7 +84,7 @@ export default function CouponsList() {
83
84
  </Link>
84
85
  {c.enabled === false && (
85
86
  <span className="rounded border px-1.5 py-0.5 text-[10px] font-medium uppercase tracking-wide text-muted-foreground">
86
- Disabled
87
+ {t("coupons.disabled")}
87
88
  </span>
88
89
  )}
89
90
  </span>
@@ -91,23 +92,23 @@ export default function CouponsList() {
91
92
  },
92
93
  {
93
94
  key: "discount_type",
94
- label: "Type",
95
+ label: t("common.type"),
95
96
  render: (c) =>
96
97
  COUPON_TYPES.find((t) => t.value === c.discount_type)?.label || c.discount_type,
97
98
  },
98
- { key: "amount", label: "Amount", render: amountLabel },
99
+ { key: "amount", label: t("common.amount"), render: amountLabel },
99
100
  {
100
101
  key: "description",
101
- label: "Description",
102
+ label: t("common.description"),
102
103
  className: "max-w-64",
103
104
  render: (c) => (
104
105
  <span className="text-muted-foreground">{truncate(c.description, 60) || "—"}</span>
105
106
  ),
106
107
  },
107
- { key: "usage", label: "Usage / Limit", render: usageLabel },
108
+ { key: "usage", label: t("coupons.usage_limit_col"), render: usageLabel },
108
109
  {
109
110
  key: "date_expires",
110
- label: "Expiry date",
111
+ label: t("coupons.expiry_date"),
111
112
  render: (c) => (c.date_expires ? formatDate(c.date_expires) : "—"),
112
113
  },
113
114
  ];
@@ -115,12 +116,12 @@ export default function CouponsList() {
115
116
  return (
116
117
  <div>
117
118
  <PageHeader
118
- title="Coupons"
119
- description="Create and manage discount codes."
119
+ title={t("routes.coupons")}
120
+ description={t("coupons.subtitle")}
120
121
  actions={
121
122
  <Button asChild>
122
123
  <Link to={href("coupons/new")}>
123
- <Plus className="mr-1 h-4 w-4" /> Add coupon
124
+ <Plus className="mr-1 h-4 w-4" /> {t("routes.coupons_new")}
124
125
  </Link>
125
126
  </Button>
126
127
  }
@@ -131,14 +132,14 @@ export default function CouponsList() {
131
132
  <Search className="pointer-events-none absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
132
133
  <Input
133
134
  className="pl-8"
134
- placeholder="Search coupons"
135
+ placeholder={t("coupons.search_placeholder")}
135
136
  value={query}
136
137
  onChange={(e) => setQuery(e.target.value)}
137
138
  />
138
139
  </div>
139
140
  {selected.length > 0 && (
140
141
  <Button variant="outline" size="sm" onClick={() => setPendingDelete("bulk")}>
141
- <Trash2 className="mr-1 h-4 w-4" /> Delete selected ({selected.length})
142
+ <Trash2 className="mr-1 h-4 w-4" /> {t("coupons.delete_selected", { count: selected.length })}
142
143
  </Button>
143
144
  )}
144
145
  </div>
@@ -154,23 +155,23 @@ export default function CouponsList() {
154
155
  onSelectChange={setSelected}
155
156
  onRowClick={(c) => navigate(href(`coupons/${c.id}`))}
156
157
  rowActions={(c) => [
157
- { label: "Edit", icon: Pencil, onClick: () => navigate(href(`coupons/${c.id}`)) },
158
- { label: "Delete", icon: Trash2, destructive: true, onClick: () => setPendingDelete(c) },
158
+ { label: t("common.edit"), icon: Pencil, onClick: () => navigate(href(`coupons/${c.id}`)) },
159
+ { label: t("common.delete"), icon: Trash2, destructive: true, onClick: () => setPendingDelete(c) },
159
160
  ]}
160
161
  pagination={{ page: list.page, hasNext: list.hasNext, onNext: list.next, onPrev: list.prev }}
161
162
  empty={{
162
163
  icon: TicketPercent,
163
- title: "No coupons yet",
164
- description: "Coupons let you offer discounts at checkout.",
164
+ title: t("coupons.empty_title"),
165
+ description: t("coupons.empty_description"),
165
166
  }}
166
167
  />
167
168
 
168
169
  <ConfirmDialog
169
170
  open={!!pendingDelete}
170
171
  onOpenChange={(o) => !o && setPendingDelete(null)}
171
- title={pendingDelete === "bulk" ? `Delete ${selected.length} coupon(s)?` : "Delete this coupon?"}
172
- description="Customers will no longer be able to use it. This cannot be undone."
173
- confirmLabel="Delete"
172
+ title={pendingDelete === "bulk" ? t("coupons.confirm_delete_many", { count: selected.length }) : t("coupons.confirm_delete_one")}
173
+ description={t("coupons.confirm_delete_description")}
174
+ confirmLabel={t("common.delete")}
174
175
  loading={deleting}
175
176
  onConfirm={doDelete}
176
177
  />