@base44/app-plugin-commerce 0.7.1 → 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 +8 -24
  9. package/skills/commerce/install/02-storefront.md +21 -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 -14
  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
@@ -21,6 +21,7 @@ import CountrySelect from "../../components/CountrySelect";
21
21
  import MoneyInput from "../../components/MoneyInput";
22
22
  import useMoney from "../../hooks/useMoney";
23
23
  import { CONTINENTS } from "../../lib/geo-data";
24
+ import { t } from "../../i18n";
24
25
 
25
26
  const NEW_LOCATION = {
26
27
  name: "",
@@ -62,14 +63,14 @@ export default function LocationEditor() {
62
63
  }, [locationId, isNew]);
63
64
 
64
65
  useEffect(() => {
65
- load().catch((e) => toast.error(e.message || "Failed to load location"));
66
+ load().catch((e) => toast.error(e.message || t("settings.location.load_failed")));
66
67
  }, [load]);
67
68
 
68
69
  const up = (patch) => setLocation((l) => ({ ...l, ...patch }));
69
70
 
70
71
  const save = async () => {
71
72
  if (!location.name?.trim()) {
72
- toast.error("Location name is required");
73
+ toast.error(t("settings.location.name_required"));
73
74
  return;
74
75
  }
75
76
  setSaving(true);
@@ -85,10 +86,10 @@ export default function LocationEditor() {
85
86
  };
86
87
  if (isNew) await base44.entities["commerce.ShippingTaxLocation"].create(payload);
87
88
  else await base44.entities["commerce.ShippingTaxLocation"].update(locationId, payload);
88
- toast.success(isNew ? "Location created" : "Location saved");
89
+ toast.success(isNew ? t("settings.location.created") : t("settings.location.saved"));
89
90
  navigate(href("settings/shipping-tax"));
90
91
  } catch (e) {
91
- toast.error(e.message || "Failed to save location");
92
+ toast.error(e.message || t("settings.location.save_failed"));
92
93
  } finally {
93
94
  setSaving(false);
94
95
  }
@@ -108,23 +109,22 @@ export default function LocationEditor() {
108
109
  return (
109
110
  <div className="max-w-3xl space-y-4">
110
111
  <PageHeader
111
- title={isNew ? "Add location" : location.name || "Edit location"}
112
- description="Manage where you sell, and the shipping and tax charged there."
112
+ title={isNew ? t("settings.shipping.add_location") : location.name || t("settings.location.edit_location")}
113
+ description={t("settings.location.header_description")}
113
114
  backHref={href("settings/shipping-tax")}
114
115
  actions={
115
116
  <Button onClick={save} disabled={saving}>
116
117
  {saving ? <Loader2 className="mr-2 h-4 w-4 animate-spin" /> : <Save className="mr-2 h-4 w-4" />}
117
- Save
118
+ {t("common.save")}
118
119
  </Button>
119
120
  }
120
121
  />
121
122
 
122
123
  <Card>
123
124
  <CardHeader className="pb-2">
124
- <CardTitle className="text-base">Location</CardTitle>
125
+ <CardTitle className="text-base">{t("settings.shipping.location")}</CardTitle>
125
126
  <CardDescription>
126
- Leave the regions empty to make this the fallback that catches every address no other
127
- location covers.
127
+ {t("settings.location.regions_hint")}
128
128
  </CardDescription>
129
129
  </CardHeader>
130
130
  <CardContent className="space-y-3">
@@ -132,31 +132,31 @@ export default function LocationEditor() {
132
132
  [1fr | 130px | 130px | 40px icon] — so inputs align across cards. */}
133
133
  <div className="grid grid-cols-[1fr_130px_130px_40px] items-center gap-2">
134
134
  <div className="col-span-2 space-y-1.5">
135
- <Label>Name</Label>
136
- <Input value={location.name} onChange={(e) => up({ name: e.target.value })} placeholder="e.g. Israel" />
135
+ <Label>{t("common.name")}</Label>
136
+ <Input value={location.name} onChange={(e) => up({ name: e.target.value })} placeholder={t("settings.location.name_placeholder")} />
137
137
  </div>
138
138
  <div className="space-y-1.5">
139
- <Label>Match order</Label>
139
+ <Label>{t("settings.location.match_order")}</Label>
140
140
  <Input type="number" value={location.order ?? 0} onChange={(e) => up({ order: Number(e.target.value) || 0 })} />
141
141
  </div>
142
142
  <div />
143
143
  </div>
144
144
 
145
145
  <div className="space-y-2">
146
- <Label className="block">Regions</Label>
146
+ <Label className="block">{t("settings.shipping.regions")}</Label>
147
147
  {regions.map((region, i) => (
148
148
  <div key={i} className="grid grid-cols-[130px_1fr_40px] items-center gap-2">
149
149
  <Select value={region.type || "country"} onValueChange={(v) => setRegion(i, { type: v, code: "" })}>
150
150
  <SelectTrigger><SelectValue /></SelectTrigger>
151
151
  <SelectContent>
152
- <SelectItem value="country">Country</SelectItem>
153
- <SelectItem value="continent">Continent</SelectItem>
154
- <SelectItem value="state">State</SelectItem>
152
+ <SelectItem value="country">{t("address.country")}</SelectItem>
153
+ <SelectItem value="continent">{t("settings.location.continent")}</SelectItem>
154
+ <SelectItem value="state">{t("settings.location.state")}</SelectItem>
155
155
  </SelectContent>
156
156
  </Select>
157
157
  {region.type === "continent" ? (
158
158
  <Select value={region.code || ""} onValueChange={(v) => setRegion(i, { code: v })}>
159
- <SelectTrigger><SelectValue placeholder="Select continent…" /></SelectTrigger>
159
+ <SelectTrigger><SelectValue placeholder={t("settings.location.select_continent")} /></SelectTrigger>
160
160
  <SelectContent>
161
161
  {CONTINENTS.map((c) => (
162
162
  <SelectItem key={c.code} value={c.code}>{c.name}</SelectItem>
@@ -165,7 +165,7 @@ export default function LocationEditor() {
165
165
  </Select>
166
166
  ) : region.type === "state" ? (
167
167
  <Input
168
- placeholder="COUNTRY:STATE — e.g. US:CA"
168
+ placeholder={t("settings.location.state_placeholder")}
169
169
  value={region.code || ""}
170
170
  onChange={(e) => setRegion(i, { code: e.target.value.toUpperCase() })}
171
171
  />
@@ -179,7 +179,7 @@ export default function LocationEditor() {
179
179
  ))}
180
180
  <Button type="button" variant="outline" size="sm" onClick={() => up({ regions: [...regions, { type: "country", code: "" }] })}>
181
181
  <Plus className="mr-2 h-4 w-4" />
182
- Add region
182
+ {t("settings.location.add_region")}
183
183
  </Button>
184
184
  </div>
185
185
  </CardContent>
@@ -187,34 +187,33 @@ export default function LocationEditor() {
187
187
 
188
188
  <Card>
189
189
  <CardHeader className="pb-2">
190
- <CardTitle className="text-base">Shipping rates</CardTitle>
190
+ <CardTitle className="text-base">{t("settings.shipping.shipping_rates")}</CardTitle>
191
191
  <CardDescription>
192
- Every rate is a delivery option at checkout. A "Free over" amount makes the rate free once
193
- the (discounted) items subtotal reaches it. No rates means nothing ships here.
192
+ {t("settings.location.rates_hint")}
194
193
  </CardDescription>
195
194
  </CardHeader>
196
195
  <CardContent className="space-y-2">
197
196
  {rates.length > 0 && (
198
197
  <div className="grid grid-cols-[1fr_130px_130px_40px] items-center gap-2 px-1 text-xs font-medium uppercase tracking-wide text-muted-foreground">
199
- <span>Delivery option</span>
200
- <span>Cost</span>
201
- <span>Free over</span>
198
+ <span>{t("settings.location.delivery_option")}</span>
199
+ <span>{t("settings.location.cost")}</span>
200
+ <span>{t("settings.location.free_over")}</span>
202
201
  <span />
203
202
  </div>
204
203
  )}
205
204
  {rates.map((rate, i) => (
206
205
  <div key={rate.id || i} className="grid grid-cols-[1fr_130px_130px_40px] items-center gap-2">
207
206
  <Input
208
- placeholder="e.g. Standard delivery"
207
+ placeholder={t("settings.location.rate_name_placeholder")}
209
208
  value={rate.name || ""}
210
209
  onChange={(e) => setRate(i, { name: e.target.value })}
211
210
  />
212
- <MoneyInput value={rate.cost} onChange={(v) => setRate(i, { cost: v ?? 0 })} placeholder="Cost" />
211
+ <MoneyInput value={rate.cost} onChange={(v) => setRate(i, { cost: v ?? 0 })} placeholder={t("settings.location.cost")} />
213
212
  <Input
214
213
  type="number"
215
214
  min={0}
216
- placeholder="Never"
217
- title="Items subtotal (after discounts) at which this rate becomes free"
215
+ placeholder={t("settings.location.never")}
216
+ title={t("settings.location.free_over_title")}
218
217
  value={rate.free_over ?? ""}
219
218
  onChange={(e) => setRate(i, { free_over: num(e.target.value) })}
220
219
  />
@@ -230,18 +229,16 @@ export default function LocationEditor() {
230
229
  onClick={() => up({ shipping_rates: [...rates, { id: crypto.randomUUID(), name: "", cost: 0, free_over: null }] })}
231
230
  >
232
231
  <Plus className="mr-2 h-4 w-4" />
233
- Add rate
232
+ {t("settings.location.add_rate")}
234
233
  </Button>
235
234
  </CardContent>
236
235
  </Card>
237
236
 
238
237
  <Card>
239
238
  <CardHeader className="pb-2">
240
- <CardTitle className="text-base">Tax</CardTitle>
239
+ <CardTitle className="text-base">{t("settings.shipping.tax")}</CardTitle>
241
240
  <CardDescription>
242
- Tax groups bundle percentage rates — a group's tax is the sum of its rates. Products
243
- charge the <strong>Products</strong> group unless their own Tax group names another;
244
- add groups for goods taxed differently. Tax on shipping is set separately below.
241
+ {t("settings.location.tax_hint", { group: "Products" })}
245
242
  </CardDescription>
246
243
  </CardHeader>
247
244
  <CardContent className="space-y-4">
@@ -255,12 +252,12 @@ export default function LocationEditor() {
255
252
  <Input
256
253
  className="h-8 max-w-xs bg-background font-medium"
257
254
  value={group.name}
258
- placeholder="Group name"
255
+ placeholder={t("settings.location.group_name_placeholder")}
259
256
  disabled={isDefault}
260
257
  onChange={(e) => setGroup(i, { name: e.target.value })}
261
258
  />
262
259
  <span className="text-xs text-muted-foreground">
263
- {isDefault ? "Default group — products without a Tax group charge this" : "Tax group"}
260
+ {isDefault ? t("settings.location.default_group_hint") : t("settings.location.tax_group")}
264
261
  </span>
265
262
  </div>
266
263
  {!isDefault && (
@@ -272,13 +269,17 @@ export default function LocationEditor() {
272
269
  <div className="space-y-2 p-3">
273
270
  {!groupRates.length && (
274
271
  <p className="text-xs text-muted-foreground">
275
- No rates — {isDefault ? "products" : `${group.name.trim() || "these"} items`} are not taxed at this location.
272
+ {isDefault
273
+ ? t("settings.location.no_tax_rates_default")
274
+ : group.name.trim()
275
+ ? t("settings.location.no_tax_rates", { name: group.name.trim() })
276
+ : t("settings.location.no_tax_rates_unnamed")}
276
277
  </p>
277
278
  )}
278
279
  {groupRates.map((r, j) => (
279
280
  <div key={j} className="grid grid-cols-[1fr_130px_40px] items-center gap-2">
280
281
  <Input
281
- placeholder="Rate name — e.g. Sales tax, VAT"
282
+ placeholder={t("settings.location.rate_placeholder")}
282
283
  value={r.name || ""}
283
284
  onChange={(e) => setGroup(i, { rates: groupRates.map((x, k) => (k === j ? { ...x, name: e.target.value } : x)) })}
284
285
  />
@@ -309,7 +310,7 @@ export default function LocationEditor() {
309
310
  onClick={() => setGroup(i, { rates: [...groupRates, { name: "", rate: 0 }] })}
310
311
  >
311
312
  <Plus className="mr-2 h-4 w-4" />
312
- Add rate
313
+ {t("settings.location.add_rate")}
313
314
  </Button>
314
315
  </div>
315
316
  </div>
@@ -323,16 +324,16 @@ export default function LocationEditor() {
323
324
  onClick={() => up({ tax_groups: [...groups, { name: "", rates: [{ name: "", rate: 0 }] }] })}
324
325
  >
325
326
  <Plus className="mr-2 h-4 w-4" />
326
- Add tax group
327
+ {t("settings.location.add_tax_group")}
327
328
  </Button>
328
329
 
329
330
  <div className="rounded-lg border">
330
331
  <div className="border-b bg-muted/40 px-3 py-2">
331
- <p className="text-sm font-medium">Tax on shipping</p>
332
+ <p className="text-sm font-medium">{t("settings.location.tax_on_shipping")}</p>
332
333
  </div>
333
334
  <div className="grid grid-cols-[1fr_130px_130px_40px] items-center gap-2 p-3">
334
335
  <p className="text-xs text-muted-foreground">
335
- Applied to the shipping cost at this location — separate from the product groups above.
336
+ {t("settings.location.tax_on_shipping_hint")}
336
337
  </p>
337
338
  <Select
338
339
  value={shippingTax?.type || "none"}
@@ -340,9 +341,9 @@ export default function LocationEditor() {
340
341
  >
341
342
  <SelectTrigger><SelectValue /></SelectTrigger>
342
343
  <SelectContent>
343
- <SelectItem value="none">Not taxed</SelectItem>
344
- <SelectItem value="percent">Rate %</SelectItem>
345
- <SelectItem value="fixed">Exact amount</SelectItem>
344
+ <SelectItem value="none">{t("settings.location.not_taxed")}</SelectItem>
345
+ <SelectItem value="percent">{t("settings.location.rate_percent")}</SelectItem>
346
+ <SelectItem value="fixed">{t("settings.location.exact_amount")}</SelectItem>
346
347
  </SelectContent>
347
348
  </Select>
348
349
  {shippingTax ? (
@@ -368,7 +369,7 @@ export default function LocationEditor() {
368
369
  </div>
369
370
 
370
371
  <p className="text-xs text-muted-foreground">
371
- Example: {money.format(100)} in the Products group at 18% charges {money.format(18)} tax.
372
+ {t("settings.location.tax_example", { base: money.format(100), tax: money.format(18) })}
372
373
  </p>
373
374
  </CardContent>
374
375
  </Card>
@@ -16,6 +16,7 @@ import { ArrowDown, ArrowUp, Loader2, Plus, Settings2, Trash2 } from "lucide-rea
16
16
  import { toast } from "sonner";
17
17
  import { base44 } from "../../lib/api";
18
18
  import useAsync from "../../hooks/useAsync";
19
+ import { t } from "../../i18n";
19
20
 
20
21
  const emptyBank = () => ({ account_name: "", account_number: "", bank_name: "", sort_code: "", iban: "", bic: "" });
21
22
 
@@ -42,7 +43,7 @@ export default function PaymentsSettings() {
42
43
  await base44.entities["commerce.PaymentGateway"].update(g.id, { enabled });
43
44
  gateways.refetch();
44
45
  } catch (err) {
45
- toast.error(err.message || "Failed to update gateway");
46
+ toast.error(err.message || t("settings.payments.update_failed"));
46
47
  }
47
48
  };
48
49
 
@@ -59,21 +60,21 @@ export default function PaymentsSettings() {
59
60
  ]);
60
61
  gateways.refetch();
61
62
  } catch (err) {
62
- toast.error(err.message || "Failed to reorder");
63
+ toast.error(err.message || t("settings.payments.reorder_failed"));
63
64
  } finally {
64
65
  setBusy(false);
65
66
  }
66
67
  };
67
68
 
68
69
  const remove = async (g) => {
69
- if (!window.confirm(`Delete the "${g.title || g.slug}" payment option?`)) return;
70
+ if (!window.confirm(t("settings.payments.delete_confirm", { title: g.title || g.slug }))) return;
70
71
  setBusy(true);
71
72
  try {
72
73
  await base44.entities["commerce.PaymentGateway"].delete(g.id);
73
- toast.success("Payment method deleted");
74
+ toast.success(t("settings.payments.deleted"));
74
75
  gateways.refetch();
75
76
  } catch (err) {
76
- toast.error(err.message || "Failed to delete");
77
+ toast.error(err.message || t("settings.payments.delete_failed"));
77
78
  } finally {
78
79
  setBusy(false);
79
80
  }
@@ -88,7 +89,7 @@ export default function PaymentsSettings() {
88
89
  const saveGateway = async () => {
89
90
  const g = dialog.gateway;
90
91
  if (!String(g.title || "").trim()) {
91
- toast.error("Give the payment method a title");
92
+ toast.error(t("settings.payments.title_required"));
92
93
  return;
93
94
  }
94
95
  setSaving(true);
@@ -104,19 +105,19 @@ export default function PaymentsSettings() {
104
105
  method_description: g.description || "",
105
106
  settings: g.settings || {},
106
107
  });
107
- toast.success("Payment method added");
108
+ toast.success(t("settings.payments.added"));
108
109
  } else {
109
110
  await base44.entities["commerce.PaymentGateway"].update(g.id, {
110
111
  title: g.title,
111
112
  description: g.description,
112
113
  settings: g.settings || {},
113
114
  });
114
- toast.success("Payment method saved");
115
+ toast.success(t("settings.payments.saved"));
115
116
  }
116
117
  setDialog(null);
117
118
  gateways.refetch();
118
119
  } catch (err) {
119
- toast.error(err.message || "Failed to save");
120
+ toast.error(err.message || t("settings.save_failed"));
120
121
  } finally {
121
122
  setSaving(false);
122
123
  }
@@ -135,16 +136,14 @@ export default function PaymentsSettings() {
135
136
  <Card>
136
137
  <CardHeader className="flex-row items-start justify-between gap-4 space-y-0 pb-3">
137
138
  <div className="space-y-1.5">
138
- <CardTitle>Checkout options</CardTitle>
139
+ <CardTitle>{t("settings.payments.checkout_options")}</CardTitle>
139
140
  <CardDescription>
140
- Which payment options customers can pick at checkout, in this order, and the wording
141
- they see. Every option except Credit card settles outside the store: the order goes
142
- on-hold with the option's description as instructions, and you move it on once paid.
141
+ {t("settings.payments.checkout_options_description")}
143
142
  </CardDescription>
144
143
  </div>
145
144
  <Button size="sm" onClick={openNew}>
146
145
  <Plus className="mr-2 h-4 w-4" />
147
- Add payment method
146
+ {t("settings.payments.add_method")}
148
147
  </Button>
149
148
  </CardHeader>
150
149
  <CardContent className="divide-y border-t p-0">
@@ -187,7 +186,7 @@ export default function PaymentsSettings() {
187
186
  onClick={() => setDialog({ gateway: { ...g, settings: { ...(g.settings || {}) } } })}
188
187
  >
189
188
  <Settings2 className="mr-2 h-4 w-4" />
190
- Edit
189
+ {t("common.edit")}
191
190
  </Button>
192
191
  {!BUILT_IN_SLUGS.has(g.slug) && (
193
192
  <Button
@@ -195,7 +194,7 @@ export default function PaymentsSettings() {
195
194
  size="icon"
196
195
  className="h-8 w-8 text-muted-foreground"
197
196
  disabled={busy}
198
- title="Delete"
197
+ title={t("common.delete")}
199
198
  onClick={() => remove(g)}
200
199
  >
201
200
  <Trash2 className="h-3.5 w-3.5" />
@@ -224,20 +223,20 @@ export default function PaymentsSettings() {
224
223
  <DialogContent className="max-h-[85vh] overflow-y-auto">
225
224
  <DialogHeader>
226
225
  <DialogTitle>
227
- {dialog.isNew ? "Add payment method" : dialog.gateway.method_title || dialog.gateway.title}
226
+ {dialog.isNew ? t("settings.payments.add_method") : dialog.gateway.method_title || dialog.gateway.title}
228
227
  </DialogTitle>
229
228
  </DialogHeader>
230
229
  <div className="space-y-4">
231
230
  <div className="space-y-1.5">
232
- <Label>Title</Label>
231
+ <Label>{t("settings.payments.title_label")}</Label>
233
232
  <Input
234
233
  value={dialog.gateway.title || ""}
235
234
  onChange={(e) => setDialog({ ...dialog, gateway: { ...dialog.gateway, title: e.target.value } })}
236
235
  />
237
- <p className="text-xs text-muted-foreground">Shown to customers during checkout.</p>
236
+ <p className="text-xs text-muted-foreground">{t("settings.payments.title_hint")}</p>
238
237
  </div>
239
238
  <div className="space-y-1.5">
240
- <Label>Description</Label>
239
+ <Label>{t("common.description")}</Label>
241
240
  <Textarea
242
241
  rows={2}
243
242
  value={dialog.gateway.description || ""}
@@ -245,7 +244,7 @@ export default function PaymentsSettings() {
245
244
  />
246
245
  {dialog.gateway.slug !== "card" && (
247
246
  <p className="text-xs text-muted-foreground">
248
- Also shown as the payment instructions after the order is placed.
247
+ {t("settings.payments.description_hint")}
249
248
  </p>
250
249
  )}
251
250
  </div>
@@ -260,10 +259,10 @@ export default function PaymentsSettings() {
260
259
  )}
261
260
  </div>
262
261
  <DialogFooter>
263
- <Button variant="outline" onClick={() => setDialog(null)}>Cancel</Button>
262
+ <Button variant="outline" onClick={() => setDialog(null)}>{t("common.cancel")}</Button>
264
263
  <Button onClick={saveGateway} disabled={saving}>
265
264
  {saving && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
266
- {dialog.isNew ? "Add" : "Save"}
265
+ {dialog.isNew ? t("common.add") : t("common.save")}
267
266
  </Button>
268
267
  </DialogFooter>
269
268
  </DialogContent>
@@ -280,28 +279,28 @@ function BankAccountsEditor({ accounts, onChange }) {
280
279
 
281
280
  return (
282
281
  <div className="space-y-3">
283
- <Label>Bank account details</Label>
282
+ <Label>{t("settings.payments.bank_details")}</Label>
284
283
  {accounts.length === 0 && (
285
284
  <p className="text-xs text-muted-foreground">
286
- None yet — fine for cash on delivery; add one for bank transfer.
285
+ {t("settings.payments.bank_none")}
287
286
  </p>
288
287
  )}
289
288
  {accounts.map((a, i) => (
290
289
  <div key={i} className="space-y-2 rounded-md border p-3">
291
290
  <div className="flex items-center justify-between">
292
- <span className="text-sm font-medium">Account {i + 1}</span>
291
+ <span className="text-sm font-medium">{t("settings.payments.account_n", { n: i + 1 })}</span>
293
292
  <Button variant="ghost" size="icon" className="h-7 w-7" onClick={() => remove(i)}>
294
293
  <Trash2 className="h-3.5 w-3.5" />
295
294
  </Button>
296
295
  </div>
297
296
  <div className="grid gap-2 sm:grid-cols-2">
298
297
  {[
299
- ["account_name", "Account name"],
300
- ["account_number", "Account number"],
301
- ["bank_name", "Bank name"],
302
- ["sort_code", "Sort code"],
303
- ["iban", "IBAN"],
304
- ["bic", "BIC / Swift"],
298
+ ["account_name", t("settings.payments.account_name")],
299
+ ["account_number", t("settings.payments.account_number")],
300
+ ["bank_name", t("settings.payments.bank_name")],
301
+ ["sort_code", t("settings.payments.sort_code")],
302
+ ["iban", t("settings.payments.iban")],
303
+ ["bic", t("settings.payments.bic")],
305
304
  ].map(([key, label]) => (
306
305
  <div key={key} className="space-y-1">
307
306
  <Label className="text-xs">{label}</Label>
@@ -313,7 +312,7 @@ function BankAccountsEditor({ accounts, onChange }) {
313
312
  ))}
314
313
  <Button variant="outline" size="sm" onClick={add}>
315
314
  <Plus className="mr-2 h-4 w-4" />
316
- Add account
315
+ {t("settings.payments.add_account")}
317
316
  </Button>
318
317
  </div>
319
318
  );
@@ -2,14 +2,15 @@ import React from "react";
2
2
  import { Outlet, useLocation, useNavigate } from "react-router-dom";
3
3
  import { useAdminHref } from "../../context/BasePathContext";
4
4
  import PageHeader from "../../components/PageHeader";
5
+ import { t } from "../../i18n";
5
6
 
6
7
  const TABS = [
7
- { seg: "general", label: "General" },
8
- { seg: "inventory", label: "Inventory" },
9
- { seg: "shipping-tax", label: "Shipping & Tax" },
10
- { seg: "payments", label: "Payments" },
11
- { seg: "emails", label: "Emails" },
12
- { seg: "webhooks", label: "Webhooks" },
8
+ { seg: "general", label: "routes.settings_general" },
9
+ { seg: "inventory", label: "routes.settings_inventory" },
10
+ { seg: "shipping-tax", label: "routes.settings_shipping_tax" },
11
+ { seg: "payments", label: "routes.settings_payments" },
12
+ { seg: "emails", label: "routes.settings_emails" },
13
+ { seg: "webhooks", label: "routes.settings_webhooks" },
13
14
  ];
14
15
 
15
16
  /** Layout route for /settings/*: horizontal group tabs + nested page via <Outlet/>. */
@@ -24,20 +25,20 @@ export default function SettingsLayout() {
24
25
 
25
26
  return (
26
27
  <div className="space-y-4">
27
- <PageHeader title="Settings" description="Configure how your store works." />
28
+ <PageHeader title={t("routes.settings")} description={t("settings.description")} />
28
29
  <div className="flex flex-wrap gap-1 border-b">
29
- {TABS.map((t) => (
30
+ {TABS.map((tab) => (
30
31
  <button
31
- key={t.seg}
32
+ key={tab.seg}
32
33
  type="button"
33
- onClick={() => navigate(href(`settings/${t.seg}`))}
34
+ onClick={() => navigate(href(`settings/${tab.seg}`))}
34
35
  className={`-mb-px border-b-2 px-3 py-2 text-sm font-medium transition-colors ${
35
- current === t.seg
36
+ current === tab.seg
36
37
  ? "border-primary text-foreground"
37
38
  : "border-transparent text-muted-foreground hover:text-foreground"
38
39
  }`}
39
40
  >
40
- {t.label}
41
+ {t(tab.label)}
41
42
  </button>
42
43
  ))}
43
44
  </div>