@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
@@ -18,6 +18,7 @@ import {
18
18
  } from "@/components/ui/select";
19
19
  import { Loader2 } from "lucide-react";
20
20
 
21
+ import { t } from "../../../i18n";
21
22
  import { call, base44 } from "../../../lib/api";
22
23
  import SearchSelect from "../../../components/SearchSelect";
23
24
  import { isVariable, variationLabel } from "../../../lib/product-utils";
@@ -50,7 +51,7 @@ export default function AddProductDialog({ open, onOpenChange, onAdd }) {
50
51
  return rows.map((p) => ({
51
52
  value: p.id,
52
53
  label: p.name,
53
- meta: p.sku ? `SKU: ${p.sku}` : undefined,
54
+ meta: p.sku ? t("orders.sku", { sku: p.sku }) : undefined,
54
55
  product: p,
55
56
  }));
56
57
  };
@@ -83,33 +84,33 @@ export default function AddProductDialog({ open, onOpenChange, onAdd }) {
83
84
  <Dialog open={open} onOpenChange={onOpenChange}>
84
85
  <DialogContent className="sm:max-w-md">
85
86
  <DialogHeader>
86
- <DialogTitle>Add product</DialogTitle>
87
+ <DialogTitle>{t("routes.products_new")}</DialogTitle>
87
88
  </DialogHeader>
88
89
 
89
90
  <div className="space-y-4">
90
91
  <div className="grid gap-1.5">
91
- <Label>Product</Label>
92
+ <Label>{t("orders.product")}</Label>
92
93
  <SearchSelect
93
94
  search={searchProducts}
94
95
  value={picked}
95
96
  onChange={handlePick}
96
- placeholder="Search products…"
97
+ placeholder={t("orders.search_products")}
97
98
  />
98
99
  </div>
99
100
 
100
101
  {hasVariants && (
101
102
  <div className="grid gap-1.5">
102
- <Label>Variation</Label>
103
+ <Label>{t("orders.variation")}</Label>
103
104
  {loadingVariations ? (
104
105
  <div className="flex items-center gap-2 text-sm text-muted-foreground">
105
- <Loader2 className="h-4 w-4 animate-spin" /> Loading variations…
106
+ <Loader2 className="h-4 w-4 animate-spin" /> {t("orders.loading_variations")}
106
107
  </div>
107
108
  ) : (variations || []).length === 0 ? (
108
- <p className="text-sm text-muted-foreground">This product has no variations yet.</p>
109
+ <p className="text-sm text-muted-foreground">{t("orders.no_variations")}</p>
109
110
  ) : (
110
111
  <Select value={variationId} onValueChange={setVariationId}>
111
112
  <SelectTrigger>
112
- <SelectValue placeholder="Choose a variation…" />
113
+ <SelectValue placeholder={t("orders.choose_variation")} />
113
114
  </SelectTrigger>
114
115
  <SelectContent>
115
116
  {(variations || []).map((v) => (
@@ -124,7 +125,7 @@ export default function AddProductDialog({ open, onOpenChange, onAdd }) {
124
125
  )}
125
126
 
126
127
  <div className="grid gap-1.5">
127
- <Label>Quantity</Label>
128
+ <Label>{t("orders.quantity")}</Label>
128
129
  <Input
129
130
  type="number"
130
131
  min="1"
@@ -137,10 +138,10 @@ export default function AddProductDialog({ open, onOpenChange, onAdd }) {
137
138
 
138
139
  <DialogFooter>
139
140
  <Button variant="outline" onClick={() => onOpenChange(false)}>
140
- Cancel
141
+ {t("common.cancel")}
141
142
  </Button>
142
143
  <Button onClick={submit} disabled={!canAdd}>
143
- Add to order
144
+ {t("orders.add_to_order")}
144
145
  </Button>
145
146
  </DialogFooter>
146
147
  </DialogContent>
@@ -12,6 +12,7 @@ import {
12
12
  import { Loader2, X } from "lucide-react";
13
13
  import { toast } from "sonner";
14
14
 
15
+ import { t } from "../../../i18n";
15
16
  import { call } from "../../../lib/api";
16
17
  import SearchSelect from "../../../components/SearchSelect";
17
18
  import { formatDate } from "../../../lib/format";
@@ -29,7 +30,7 @@ export default function DownloadPermissionsPanel({ orderId, permissions, loading
29
30
  const rows = data?.rows || data || [];
30
31
  return rows
31
32
  .filter((p) => p.downloadable)
32
- .map((p) => ({ value: p.id, label: p.name, meta: p.sku ? `SKU: ${p.sku}` : undefined }));
33
+ .map((p) => ({ value: p.id, label: p.name, meta: p.sku ? t("orders.sku", { sku: p.sku }) : undefined }));
33
34
  };
34
35
 
35
36
  const grant = async () => {
@@ -37,7 +38,7 @@ export default function DownloadPermissionsPanel({ orderId, permissions, loading
37
38
  setBusy(true);
38
39
  try {
39
40
  await call("admin-orders", "grant-download", { order_id: orderId, product_id: picked.value });
40
- toast.success("Download access granted");
41
+ toast.success(t("orders.download_granted"));
41
42
  setPicked(null);
42
43
  onChanged();
43
44
  } catch {
@@ -50,7 +51,7 @@ export default function DownloadPermissionsPanel({ orderId, permissions, loading
50
51
  const revoke = async (permissionId) => {
51
52
  try {
52
53
  await call("admin-orders", "revoke-download", { order_id: orderId, permission_id: permissionId });
53
- toast.success("Download access revoked");
54
+ toast.success(t("orders.download_revoked"));
54
55
  onChanged();
55
56
  } catch {
56
57
  /* toast handled by call() */
@@ -60,7 +61,7 @@ export default function DownloadPermissionsPanel({ orderId, permissions, loading
60
61
  return (
61
62
  <Card>
62
63
  <CardHeader className="pb-3">
63
- <CardTitle className="text-base">Downloadable product permissions</CardTitle>
64
+ <CardTitle className="text-base">{t("orders.download_permissions")}</CardTitle>
64
65
  </CardHeader>
65
66
  <CardContent className="space-y-3">
66
67
  {loading ? (
@@ -68,14 +69,14 @@ export default function DownloadPermissionsPanel({ orderId, permissions, loading
68
69
  <Loader2 className="h-4 w-4 animate-spin text-muted-foreground" />
69
70
  </div>
70
71
  ) : (permissions || []).length === 0 ? (
71
- <p className="text-sm text-muted-foreground">No download permissions on this order.</p>
72
+ <p className="text-sm text-muted-foreground">{t("orders.no_download_permissions")}</p>
72
73
  ) : (
73
74
  <Table>
74
75
  <TableHeader>
75
76
  <TableRow>
76
- <TableHead>File</TableHead>
77
- <TableHead className="w-32 text-right">Remaining</TableHead>
78
- <TableHead className="w-32">Expires</TableHead>
77
+ <TableHead>{t("orders.file")}</TableHead>
78
+ <TableHead className="w-32 text-right">{t("orders.remaining")}</TableHead>
79
+ <TableHead className="w-32">{t("orders.expires")}</TableHead>
79
80
  <TableHead className="w-10" />
80
81
  </TableRow>
81
82
  </TableHeader>
@@ -85,11 +86,11 @@ export default function DownloadPermissionsPanel({ orderId, permissions, loading
85
86
  <TableCell className="text-sm">{p.download_name || p.file_url || p.product_id}</TableCell>
86
87
  <TableCell className="text-right text-sm">
87
88
  {p.downloads_remaining === -1 ? "∞" : p.downloads_remaining}
88
- <span className="text-xs text-muted-foreground"> (used {p.download_count || 0})</span>
89
+ <span className="text-xs text-muted-foreground"> {t("orders.used_count", { count: p.download_count || 0 })}</span>
89
90
  </TableCell>
90
- <TableCell className="text-sm">{p.access_expires ? formatDate(p.access_expires) : "Never"}</TableCell>
91
+ <TableCell className="text-sm">{p.access_expires ? formatDate(p.access_expires) : t("orders.never")}</TableCell>
91
92
  <TableCell>
92
- <Button variant="ghost" size="icon" className="h-7 w-7" onClick={() => revoke(p.id)} title="Revoke">
93
+ <Button variant="ghost" size="icon" className="h-7 w-7" onClick={() => revoke(p.id)} title={t("orders.revoke")}>
93
94
  <X className="h-4 w-4" />
94
95
  </Button>
95
96
  </TableCell>
@@ -105,12 +106,12 @@ export default function DownloadPermissionsPanel({ orderId, permissions, loading
105
106
  search={searchDownloadable}
106
107
  value={picked}
107
108
  onChange={setPicked}
108
- placeholder="Search downloadable products…"
109
+ placeholder={t("orders.search_downloadable")}
109
110
  />
110
111
  </div>
111
112
  <Button size="sm" onClick={grant} disabled={!picked || busy}>
112
113
  {busy && <Loader2 className="mr-1 h-3.5 w-3.5 animate-spin" />}
113
- Grant access
114
+ {t("orders.grant_access")}
114
115
  </Button>
115
116
  </div>
116
117
  </CardContent>
@@ -12,6 +12,7 @@ import {
12
12
  } from "@/components/ui/table";
13
13
  import { Package, X } from "lucide-react";
14
14
 
15
+ import { t } from "../../../i18n";
15
16
  import useMoney from "../../../hooks/useMoney";
16
17
  import { round2 } from "../../../lib/order-utils";
17
18
 
@@ -56,11 +57,11 @@ export default function LineItemsTable({ order, editable, onChange, onRemoveCoup
56
57
  <Table>
57
58
  <TableHeader>
58
59
  <TableRow>
59
- <TableHead>Item</TableHead>
60
- <TableHead className="w-28 text-right">Cost</TableHead>
61
- <TableHead className="w-20 text-right">Qty</TableHead>
62
- <TableHead className="w-28 text-right">Total</TableHead>
63
- <TableHead className="w-24 text-right">Tax</TableHead>
60
+ <TableHead>{t("orders.item")}</TableHead>
61
+ <TableHead className="w-28 text-right">{t("orders.cost")}</TableHead>
62
+ <TableHead className="w-20 text-right">{t("orders.qty")}</TableHead>
63
+ <TableHead className="w-28 text-right">{t("orders.total")}</TableHead>
64
+ <TableHead className="w-24 text-right">{t("orders.tax")}</TableHead>
64
65
  {editable && <TableHead className="w-10" />}
65
66
  </TableRow>
66
67
  </TableHeader>
@@ -68,7 +69,7 @@ export default function LineItemsTable({ order, editable, onChange, onRemoveCoup
68
69
  {lines.length === 0 && fees.length === 0 && shipping.length === 0 && (
69
70
  <TableRow>
70
71
  <TableCell colSpan={editable ? 6 : 5} className="py-8 text-center text-sm text-muted-foreground">
71
- No items. {editable ? "Use “Add product(s)” below." : ""}
72
+ {t("orders.no_items")} {editable ? t("orders.no_items_hint") : ""}
72
73
  </TableCell>
73
74
  </TableRow>
74
75
  )}
@@ -87,7 +88,7 @@ export default function LineItemsTable({ order, editable, onChange, onRemoveCoup
87
88
  <div className="min-w-0">
88
89
  <p className="truncate text-sm font-medium">{line.name}</p>
89
90
  <p className="text-xs text-muted-foreground">
90
- {line.sku && <span>SKU: {line.sku}</span>}
91
+ {line.sku && <span>{t("orders.sku", { sku: line.sku })}</span>}
91
92
  {(line.attributes || []).length > 0 && (
92
93
  <span>
93
94
  {line.sku ? " · " : ""}
@@ -142,7 +143,7 @@ export default function LineItemsTable({ order, editable, onChange, onRemoveCoup
142
143
  <TableRow key={fee.line_id} className="bg-muted/30">
143
144
  <TableCell>
144
145
  <span className="text-sm">
145
- <Badge variant="outline" className="mr-2">Fee</Badge>
146
+ <Badge variant="outline" className="mr-2">{t("orders.fee")}</Badge>
146
147
  {fee.name}
147
148
  </span>
148
149
  </TableCell>
@@ -164,7 +165,7 @@ export default function LineItemsTable({ order, editable, onChange, onRemoveCoup
164
165
  <TableRow key={line.line_id} className="bg-muted/30">
165
166
  <TableCell>
166
167
  <span className="text-sm">
167
- <Badge variant="outline" className="mr-2">Shipping</Badge>
168
+ <Badge variant="outline" className="mr-2">{t("orders.shipping")}</Badge>
168
169
  {line.method_title || line.method_id}
169
170
  </span>
170
171
  </TableCell>
@@ -186,7 +187,7 @@ export default function LineItemsTable({ order, editable, onChange, onRemoveCoup
186
187
 
187
188
  {coupons.length > 0 && (
188
189
  <div className="flex flex-wrap items-center gap-2 border-t px-3 py-2">
189
- <span className="text-xs text-muted-foreground">Coupons:</span>
190
+ <span className="text-xs text-muted-foreground">{t("orders.coupons_label")}</span>
190
191
  {coupons.map((c) => (
191
192
  <Badge key={c.code} variant="secondary" className="gap-1 pr-1">
192
193
  {c.code} (−{format(c.discount || 0)})
@@ -195,7 +196,7 @@ export default function LineItemsTable({ order, editable, onChange, onRemoveCoup
195
196
  className="rounded-full p-0.5 hover:bg-muted-foreground/20 disabled:opacity-50"
196
197
  disabled={removingCoupon === c.code}
197
198
  onClick={() => onRemoveCoupon(c.code)}
198
- title="Remove coupon"
199
+ title={t("orders.remove_coupon")}
199
200
  >
200
201
  <X className="h-3 w-3" />
201
202
  </button>
@@ -12,6 +12,7 @@ import {
12
12
  import { Loader2, X } from "lucide-react";
13
13
  import { toast } from "sonner";
14
14
 
15
+ import { t } from "../../../i18n";
15
16
  import { call } from "../../../lib/api";
16
17
  import { formatDateTime } from "../../../lib/format";
17
18
 
@@ -40,7 +41,7 @@ export default function OrderNotesPanel({ orderId, notes, loading, onChanged })
40
41
  note: text.trim(),
41
42
  is_customer_note: kind === "customer",
42
43
  });
43
- toast.success(kind === "customer" ? "Note sent to customer" : "Note added");
44
+ toast.success(kind === "customer" ? t("orders.note_sent") : t("orders.note_added"));
44
45
  setText("");
45
46
  onChanged();
46
47
  } catch {
@@ -62,7 +63,7 @@ export default function OrderNotesPanel({ orderId, notes, loading, onChanged })
62
63
  return (
63
64
  <Card>
64
65
  <CardHeader className="pb-3">
65
- <CardTitle className="text-base">Order notes</CardTitle>
66
+ <CardTitle className="text-base">{t("orders.order_notes")}</CardTitle>
66
67
  </CardHeader>
67
68
  <CardContent className="space-y-3">
68
69
  {loading ? (
@@ -70,7 +71,7 @@ export default function OrderNotesPanel({ orderId, notes, loading, onChanged })
70
71
  <Loader2 className="h-4 w-4 animate-spin text-muted-foreground" />
71
72
  </div>
72
73
  ) : (notes || []).length === 0 ? (
73
- <p className="text-sm text-muted-foreground">No notes yet.</p>
74
+ <p className="text-sm text-muted-foreground">{t("orders.no_notes")}</p>
74
75
  ) : (
75
76
  <ul className="max-h-96 space-y-2 overflow-y-auto pr-1">
76
77
  {notes.map((n) => (
@@ -78,14 +79,14 @@ export default function OrderNotesPanel({ orderId, notes, loading, onChanged })
78
79
  <p className="whitespace-pre-wrap">{n.note}</p>
79
80
  <p className="mt-1.5 text-xs text-muted-foreground">
80
81
  {formatDateTime(n.created_date)}
81
- {n.added_by && n.added_by !== "system" ? ` · ${n.added_by}` : n.added_by === "system" ? " · system" : ""}
82
- {n.is_customer_note ? " · sent to customer" : ""}
82
+ {n.added_by && n.added_by !== "system" ? ` · ${n.added_by}` : n.added_by === "system" ? ` · ${t("orders.system")}` : ""}
83
+ {n.is_customer_note ? ` · ${t("orders.sent_to_customer")}` : ""}
83
84
  </p>
84
85
  <button
85
86
  type="button"
86
87
  onClick={() => deleteNote(n.id)}
87
88
  className="absolute right-1.5 top-1.5 hidden rounded p-0.5 hover:bg-black/5 group-hover:block"
88
- title="Delete note"
89
+ title={t("orders.delete_note")}
89
90
  >
90
91
  <X className="h-3.5 w-3.5 text-muted-foreground" />
91
92
  </button>
@@ -97,7 +98,7 @@ export default function OrderNotesPanel({ orderId, notes, loading, onChanged })
97
98
  <div className="space-y-2 border-t pt-3">
98
99
  <Textarea
99
100
  rows={3}
100
- placeholder="Add a note…"
101
+ placeholder={t("orders.add_note_placeholder")}
101
102
  value={text}
102
103
  onChange={(e) => setText(e.target.value)}
103
104
  />
@@ -107,13 +108,13 @@ export default function OrderNotesPanel({ orderId, notes, loading, onChanged })
107
108
  <SelectValue />
108
109
  </SelectTrigger>
109
110
  <SelectContent>
110
- <SelectItem value="private">Private note</SelectItem>
111
- <SelectItem value="customer">Note to customer</SelectItem>
111
+ <SelectItem value="private">{t("orders.private_note")}</SelectItem>
112
+ <SelectItem value="customer">{t("orders.note_to_customer")}</SelectItem>
112
113
  </SelectContent>
113
114
  </Select>
114
115
  <Button size="sm" onClick={addNote} disabled={busy || !text.trim()}>
115
116
  {busy && <Loader2 className="mr-1 h-3.5 w-3.5 animate-spin" />}
116
- Add
117
+ {t("common.add")}
117
118
  </Button>
118
119
  </div>
119
120
  </div>
@@ -6,6 +6,7 @@ import { Input } from "@/components/ui/input";
6
6
  import { Check, Copy, CreditCard, ExternalLink, Loader2, RotateCw } from "lucide-react";
7
7
  import { toast } from "sonner";
8
8
 
9
+ import { t } from "../../../i18n";
9
10
  import { call } from "../../../lib/api";
10
11
  import useMoney from "../../../hooks/useMoney";
11
12
  import { formatDateTime } from "../../../lib/format";
@@ -47,7 +48,7 @@ export default function PaymentPanel({ order, onChanged }) {
47
48
  setLink(data?.url || "");
48
49
  setCopied(false);
49
50
  if (data?.url) {
50
- toast.success("Payment link created");
51
+ toast.success(t("orders.payment_link_created"));
51
52
  if (willSwitchMethod) onChanged?.(); // the order's method changed
52
53
  }
53
54
  } catch {
@@ -62,10 +63,10 @@ export default function PaymentPanel({ order, onChanged }) {
62
63
  try {
63
64
  const data = await call("payments", "verify", { order_id: order.id });
64
65
  if (data?.paid) {
65
- toast.success("Payment confirmed");
66
+ toast.success(t("orders.payment_confirmed"));
66
67
  onChanged?.();
67
68
  } else {
68
- toast.info("No payment received yet");
69
+ toast.info(t("orders.no_payment_yet"));
69
70
  }
70
71
  } catch {
71
72
  /* toast handled by call() */
@@ -78,37 +79,37 @@ export default function PaymentPanel({ order, onChanged }) {
78
79
  try {
79
80
  await navigator.clipboard.writeText(link);
80
81
  setCopied(true);
81
- toast.success("Link copied");
82
+ toast.success(t("orders.link_copied"));
82
83
  } catch {
83
- toast.error("Could not copy — select the link and copy it manually");
84
+ toast.error(t("orders.copy_failed"));
84
85
  }
85
86
  };
86
87
 
87
88
  return (
88
89
  <Card>
89
90
  <CardHeader className="flex-row items-center justify-between space-y-0 pb-3">
90
- <CardTitle className="text-base">Payment</CardTitle>
91
+ <CardTitle className="text-base">{t("orders.payment")}</CardTitle>
91
92
  {paid ? (
92
- <Badge variant="secondary" className="font-normal">Paid</Badge>
93
+ <Badge variant="secondary" className="font-normal">{t("orders.paid")}</Badge>
93
94
  ) : (
94
- <Badge variant="outline" className="font-normal">Awaiting payment</Badge>
95
+ <Badge variant="outline" className="font-normal">{t("orders.awaiting_payment")}</Badge>
95
96
  )}
96
97
  </CardHeader>
97
98
  <CardContent className="space-y-3">
98
99
  <dl className="grid grid-cols-[auto_1fr] gap-x-3 gap-y-1 text-sm">
99
- <dt className="text-muted-foreground">Method</dt>
100
+ <dt className="text-muted-foreground">{t("orders.method")}</dt>
100
101
  <dd>{order.payment_method_title || order.payment_method || "—"}</dd>
101
- <dt className="text-muted-foreground">Total</dt>
102
+ <dt className="text-muted-foreground">{t("orders.total")}</dt>
102
103
  <dd>{format(order.total || 0)}</dd>
103
104
  {order.date_paid && (
104
105
  <>
105
- <dt className="text-muted-foreground">Paid</dt>
106
+ <dt className="text-muted-foreground">{t("orders.paid")}</dt>
106
107
  <dd>{formatDateTime(order.date_paid)}</dd>
107
108
  </>
108
109
  )}
109
110
  {order.transaction_id && (
110
111
  <>
111
- <dt className="text-muted-foreground">Reference</dt>
112
+ <dt className="text-muted-foreground">{t("orders.reference")}</dt>
112
113
  <dd className="truncate font-mono text-xs">{order.transaction_id}</dd>
113
114
  </>
114
115
  )}
@@ -117,8 +118,8 @@ export default function PaymentPanel({ order, onChanged }) {
117
118
  {!paid && !isCard && (
118
119
  <p className="text-xs text-muted-foreground">
119
120
  {order.payment_method
120
- ? `This order is set to be paid outside the store (${order.payment_method_title || order.payment_method}), so mark it paid by moving it to Processing once the money arrives.`
121
- : "No payment method is set on this order. Take payment outside the store and move it to Processing, or send the customer a payment link."}
121
+ ? t("orders.pay_outside_hint", { method: order.payment_method_title || order.payment_method })
122
+ : t("orders.no_payment_method_hint")}
122
123
  </p>
123
124
  )}
124
125
 
@@ -136,7 +137,7 @@ export default function PaymentPanel({ order, onChanged }) {
136
137
  ) : (
137
138
  <CreditCard className="mr-1.5 h-3.5 w-3.5" />
138
139
  )}
139
- {link ? "New payment link" : "Create payment link"}
140
+ {link ? t("orders.new_payment_link") : t("orders.create_payment_link")}
140
141
  </Button>
141
142
  <Button
142
143
  variant="outline"
@@ -149,30 +150,29 @@ export default function PaymentPanel({ order, onChanged }) {
149
150
  ) : (
150
151
  <RotateCw className="mr-1.5 h-3.5 w-3.5" />
151
152
  )}
152
- Check payment
153
+ {t("orders.check_payment")}
153
154
  </Button>
154
155
  </div>
155
156
 
156
157
  {willSwitchMethod && !link && (
157
158
  <p className="text-xs text-muted-foreground">
158
159
  {order.payment_method
159
- ? `Sending a payment link switches this order from ${order.payment_method_title || order.payment_method} to card payment.`
160
- : "Sending a payment link sets this order to card payment."}
160
+ ? t("orders.link_switches_method", { method: order.payment_method_title || order.payment_method })
161
+ : t("orders.link_sets_method")}
161
162
  </p>
162
163
  )}
163
164
 
164
165
  {link && (
165
166
  <div className="space-y-1.5">
166
167
  <p className="text-xs text-muted-foreground">
167
- Send this to the customer — it opens the provider's secure payment page. The order
168
- updates itself once they pay.
168
+ {t("orders.payment_link_hint")}
169
169
  </p>
170
170
  <div className="flex gap-1.5">
171
171
  <Input readOnly value={link} className="h-8 font-mono text-xs" onFocus={(e) => e.target.select()} />
172
- <Button variant="outline" size="icon" className="h-8 w-8 shrink-0" onClick={copy} title="Copy link">
172
+ <Button variant="outline" size="icon" className="h-8 w-8 shrink-0" onClick={copy} title={t("orders.copy_link")}>
173
173
  {copied ? <Check className="h-3.5 w-3.5" /> : <Copy className="h-3.5 w-3.5" />}
174
174
  </Button>
175
- <Button variant="outline" size="icon" className="h-8 w-8 shrink-0" asChild title="Open">
175
+ <Button variant="outline" size="icon" className="h-8 w-8 shrink-0" asChild title={t("orders.open")}>
176
176
  <a href={link} target="_blank" rel="noreferrer">
177
177
  <ExternalLink className="h-3.5 w-3.5" />
178
178
  </a>
@@ -14,6 +14,7 @@ import {
14
14
  import { Loader2 } from "lucide-react";
15
15
  import { toast } from "sonner";
16
16
 
17
+ import { t } from "../../../i18n";
17
18
  import { call } from "../../../lib/api";
18
19
  import useMoney from "../../../hooks/useMoney";
19
20
  import { formatDate } from "../../../lib/format";
@@ -90,8 +91,8 @@ export default function RefundPanel({ order, refunds, onDone }) {
90
91
  });
91
92
  toast.success(
92
93
  data?.gateway_refund
93
- ? `Refunded ${format(total)} through the payment provider`
94
- : `Refunded ${format(total)}`,
94
+ ? t("orders.refunded_via_provider", { amount: format(total) })
95
+ : t("orders.refunded_amount", { amount: format(total) }),
95
96
  );
96
97
  setRows({});
97
98
  setShippingAmount(0);
@@ -106,15 +107,15 @@ export default function RefundPanel({ order, refunds, onDone }) {
106
107
 
107
108
  return (
108
109
  <div className="space-y-4 rounded-lg border bg-muted/20 p-4">
109
- <h3 className="text-sm font-semibold">Refund</h3>
110
+ <h3 className="text-sm font-semibold">{t("orders.refund")}</h3>
110
111
 
111
112
  <Table>
112
113
  <TableHeader>
113
114
  <TableRow>
114
- <TableHead>Item</TableHead>
115
- <TableHead className="w-24 text-right">Refunded</TableHead>
116
- <TableHead className="w-24 text-right">Qty</TableHead>
117
- <TableHead className="w-32 text-right">Amount</TableHead>
115
+ <TableHead>{t("orders.item")}</TableHead>
116
+ <TableHead className="w-24 text-right">{t("orders.refunded")}</TableHead>
117
+ <TableHead className="w-24 text-right">{t("orders.qty")}</TableHead>
118
+ <TableHead className="w-32 text-right">{t("common.amount")}</TableHead>
118
119
  </TableRow>
119
120
  </TableHeader>
120
121
  <TableBody>
@@ -158,7 +159,7 @@ export default function RefundPanel({ order, refunds, onDone }) {
158
159
 
159
160
  <div className="grid grid-cols-1 gap-3 sm:grid-cols-3">
160
161
  <div className="grid gap-1.5">
161
- <Label className="text-xs">Refund shipping</Label>
162
+ <Label className="text-xs">{t("orders.refund_shipping")}</Label>
162
163
  <Input
163
164
  type="number"
164
165
  step="any"
@@ -168,7 +169,7 @@ export default function RefundPanel({ order, refunds, onDone }) {
168
169
  />
169
170
  </div>
170
171
  <div className="grid gap-1.5 sm:col-span-2">
171
- <Label className="text-xs">Reason for refund (optional)</Label>
172
+ <Label className="text-xs">{t("orders.refund_reason")}</Label>
172
173
  <Input value={reason} onChange={(e) => setReason(e.target.value)} />
173
174
  </div>
174
175
  </div>
@@ -176,7 +177,7 @@ export default function RefundPanel({ order, refunds, onDone }) {
176
177
  <div className="flex flex-wrap items-center gap-4">
177
178
  <label className="flex items-center gap-2 text-sm">
178
179
  <Checkbox checked={restock} onCheckedChange={(v) => setRestock(Boolean(v))} />
179
- Restock refunded items
180
+ {t("orders.restock_items")}
180
181
  </label>
181
182
  {order.payment_method === "card" && (
182
183
  <label
@@ -184,7 +185,7 @@ export default function RefundPanel({ order, refunds, onDone }) {
184
185
  title={
185
186
  canRefundOnline
186
187
  ? undefined
187
- : "This order has no card payment on record to refund"
188
+ : t("orders.no_card_payment_tip")
188
189
  }
189
190
  >
190
191
  <Checkbox
@@ -192,7 +193,7 @@ export default function RefundPanel({ order, refunds, onDone }) {
192
193
  disabled={!canRefundOnline}
193
194
  onCheckedChange={(v) => setViaGateway(Boolean(v))}
194
195
  />
195
- Send the money back through the payment provider
196
+ {t("orders.refund_via_provider")}
196
197
  </label>
197
198
  )}
198
199
  </div>
@@ -200,27 +201,27 @@ export default function RefundPanel({ order, refunds, onDone }) {
200
201
 
201
202
  <div className="flex items-center justify-between">
202
203
  <p className="text-sm text-muted-foreground">
203
- Total available to refund: <span className="font-medium text-foreground">{format(maxRefund)}</span>
204
+ {t("orders.available_to_refund")} <span className="font-medium text-foreground">{format(maxRefund)}</span>
204
205
  </p>
205
206
  <Button onClick={submit} disabled={busy || total <= 0 || overMax} variant="destructive">
206
207
  {busy && <Loader2 className="mr-1.5 h-4 w-4 animate-spin" />}
207
- Refund {format(total)}
208
+ {t("orders.refund_amount", { amount: format(total) })}
208
209
  </Button>
209
210
  </div>
210
211
  {overMax && (
211
- <p className="text-right text-xs text-destructive">Amount exceeds the refundable total.</p>
212
+ <p className="text-right text-xs text-destructive">{t("orders.refund_over_max")}</p>
212
213
  )}
213
214
 
214
215
  {(refunds || []).length > 0 && (
215
216
  <div className="border-t pt-3">
216
- <h4 className="mb-2 text-xs font-medium text-muted-foreground">Previous refunds</h4>
217
+ <h4 className="mb-2 text-xs font-medium text-muted-foreground">{t("orders.previous_refunds")}</h4>
217
218
  <ul className="space-y-1 text-sm">
218
219
  {refunds.map((r) => (
219
220
  <li key={r.id} className="flex justify-between">
220
221
  <span>
221
222
  {formatDate(r.created_date)}
222
223
  {r.reason ? ` — ${r.reason}` : ""}
223
- {r.refunded_by ? <span className="text-muted-foreground"> by {r.refunded_by}</span> : null}
224
+ {r.refunded_by ? <span className="text-muted-foreground"> {t("orders.refunded_by", { name: r.refunded_by })}</span> : null}
224
225
  </span>
225
226
  <span className="text-destructive">− {format(r.amount || 0)}</span>
226
227
  </li>
@@ -1,13 +1,14 @@
1
1
  import React from "react";
2
2
  import { Separator } from "@/components/ui/separator";
3
+ import { t } from "../../../i18n";
3
4
  import useMoney from "../../../hooks/useMoney";
4
5
  import { orderTotals } from "../../../lib/order-utils";
5
6
 
6
7
  /** Order totals summary. Props: { order } */
7
8
  export default function TotalsBox({ order }) {
8
9
  const { format } = useMoney();
9
- const t = orderTotals(order);
10
- if (!t) return null;
10
+ const totals = orderTotals(order);
11
+ if (!totals) return null;
11
12
 
12
13
  const row = (label, value, className = "") => (
13
14
  <div className={`flex items-center justify-between py-1 text-sm ${className}`}>
@@ -20,30 +21,30 @@ export default function TotalsBox({ order }) {
20
21
 
21
22
  return (
22
23
  <div className="ml-auto w-full max-w-sm">
23
- {row("Items subtotal", format(t.itemsSubtotal))}
24
- {t.discount > 0 &&
25
- row(couponCodes ? `Coupon(s): ${couponCodes}` : "Discount", `− ${format(t.discount)}`)}
26
- {t.fees !== 0 && row("Fees", format(t.fees))}
27
- {row("Shipping", format(t.shipping))}
28
- {(t.taxLines || []).length > 0
29
- ? t.taxLines.map((tl, i) => (
24
+ {row(t("orders.items_subtotal"), format(totals.itemsSubtotal))}
25
+ {totals.discount > 0 &&
26
+ row(couponCodes ? t("orders.coupons_codes", { codes: couponCodes }) : t("orders.discount"), `− ${format(totals.discount)}`)}
27
+ {totals.fees !== 0 && row(t("orders.fees"), format(totals.fees))}
28
+ {row(t("orders.shipping"), format(totals.shipping))}
29
+ {(totals.taxLines || []).length > 0
30
+ ? totals.taxLines.map((tl, i) => (
30
31
  <div key={tl.rate_id || i} className="flex items-center justify-between py-1 text-sm">
31
- <span className="text-muted-foreground">{tl.label || "Tax"}</span>
32
+ <span className="text-muted-foreground">{tl.label || t("orders.tax")}</span>
32
33
  <span>{format((tl.tax_total || 0) + (tl.shipping_tax_total || 0))}</span>
33
34
  </div>
34
35
  ))
35
- : row("Tax", format(t.taxTotal))}
36
+ : row(t("orders.tax"), format(totals.taxTotal))}
36
37
  <Separator className="my-1.5" />
37
38
  <div className="flex items-center justify-between py-1 font-semibold">
38
- <span>Order total</span>
39
- <span>{format(t.total)}</span>
39
+ <span>{t("orders.order_total")}</span>
40
+ <span>{format(totals.total)}</span>
40
41
  </div>
41
- {t.refunded > 0 && (
42
+ {totals.refunded > 0 && (
42
43
  <>
43
- {row("Refunded", `− ${format(t.refunded)}`, "text-destructive")}
44
+ {row(t("orders.refunded"), `− ${format(totals.refunded)}`, "text-destructive")}
44
45
  <div className="flex items-center justify-between py-1 text-sm font-medium">
45
- <span>Net payment</span>
46
- <span>{format(t.net)}</span>
46
+ <span>{t("orders.net_payment")}</span>
47
+ <span>{format(totals.net)}</span>
47
48
  </div>
48
49
  </>
49
50
  )}