@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
@@ -18,6 +18,7 @@ import {
18
18
  } from "recharts";
19
19
 
20
20
  import { call } from "../../lib/api";
21
+ import { t } from "../../i18n";
21
22
  import useAsync from "../../hooks/useAsync";
22
23
  import useRealtime from "../../hooks/useRealtime";
23
24
  import useMoney from "../../hooks/useMoney";
@@ -105,13 +106,13 @@ function SalesTab({ range, setRange }) {
105
106
  );
106
107
 
107
108
  const stats = [
108
- { label: "Gross sales", value: money.format(num(totals.gross_sales, totals.gross)) },
109
- { label: "Net sales", value: money.format(num(totals.net_sales, totals.net)) },
110
- { label: "Orders", value: num(totals.orders, totals.orders_count) },
111
- { label: "Items sold", value: num(totals.items, totals.items_sold) },
112
- { label: "Refunded", value: money.format(num(totals.refunds, totals.refunded)) },
113
- { label: "Discounted", value: money.format(num(totals.discount, totals.discounted)) },
114
- { label: "Shipping", value: money.format(num(totals.shipping, totals.shipping_total)) },
109
+ { label: t("reports.gross_sales"), value: money.format(num(totals.gross_sales, totals.gross)) },
110
+ { label: t("reports.net_sales"), value: money.format(num(totals.net_sales, totals.net)) },
111
+ { label: t("routes.orders"), value: num(totals.orders, totals.orders_count) },
112
+ { label: t("reports.items_sold"), value: num(totals.items, totals.items_sold) },
113
+ { label: t("status.order.refunded"), value: money.format(num(totals.refunds, totals.refunded)) },
114
+ { label: t("reports.discounted"), value: money.format(num(totals.discount, totals.discounted)) },
115
+ { label: t("reports.shipping"), value: money.format(num(totals.shipping, totals.shipping_total)) },
115
116
  ];
116
117
 
117
118
  return (
@@ -127,7 +128,7 @@ function SalesTab({ range, setRange }) {
127
128
  <Card>
128
129
  <CardHeader className="pb-2">
129
130
  <CardTitle className="text-base">
130
- Sales by {interval}
131
+ {t(`reports.sales_by_${interval}`)}
131
132
  </CardTitle>
132
133
  </CardHeader>
133
134
  <CardContent>
@@ -135,7 +136,7 @@ function SalesTab({ range, setRange }) {
135
136
  <Skeleton className="h-72 w-full" />
136
137
  ) : series.length === 0 ? (
137
138
  <p className="py-16 text-center text-sm text-muted-foreground">
138
- No sales in this period.
139
+ {t("reports.no_sales_in_period")}
139
140
  </p>
140
141
  ) : (
141
142
  <ResponsiveContainer width="100%" height={300}>
@@ -151,7 +152,7 @@ function SalesTab({ range, setRange }) {
151
152
  <YAxis yAxisId="count" orientation="right" tick={{ fontSize: 12 }} allowDecimals={false} />
152
153
  <Tooltip
153
154
  formatter={(value, name) =>
154
- name === "Orders" ? [value, name] : [money.format(value), name]
155
+ name === t("routes.orders") ? [value, name] : [money.format(value), name]
155
156
  }
156
157
  />
157
158
  <Legend />
@@ -159,7 +160,7 @@ function SalesTab({ range, setRange }) {
159
160
  yAxisId="money"
160
161
  type="monotone"
161
162
  dataKey="net"
162
- name="Net sales"
163
+ name={t("reports.net_sales")}
163
164
  stroke="#6366f1"
164
165
  strokeWidth={2}
165
166
  dot={false}
@@ -168,7 +169,7 @@ function SalesTab({ range, setRange }) {
168
169
  yAxisId="count"
169
170
  type="monotone"
170
171
  dataKey="orders"
171
- name="Orders"
172
+ name={t("routes.orders")}
172
173
  stroke="#22c55e"
173
174
  strokeWidth={2}
174
175
  dot={false}
@@ -219,14 +220,14 @@ function TopSellersTab({ range, setRange }) {
219
220
 
220
221
  <Card>
221
222
  <CardHeader className="pb-2">
222
- <CardTitle className="text-base">Top sellers by quantity</CardTitle>
223
+ <CardTitle className="text-base">{t("reports.top_sellers_by_quantity")}</CardTitle>
223
224
  </CardHeader>
224
225
  <CardContent>
225
226
  {loading ? (
226
227
  <Skeleton className="h-64 w-full" />
227
228
  ) : rows.length === 0 ? (
228
229
  <p className="py-16 text-center text-sm text-muted-foreground">
229
- No sales in this period.
230
+ {t("reports.no_sales_in_period")}
230
231
  </p>
231
232
  ) : (
232
233
  <ResponsiveContainer width="100%" height={Math.max(220, rows.length * 36)}>
@@ -239,8 +240,8 @@ function TopSellersTab({ range, setRange }) {
239
240
  width={180}
240
241
  tick={{ fontSize: 12 }}
241
242
  />
242
- <Tooltip formatter={(v, name) => (name === "Quantity" ? [v, name] : [money.format(v), name])} />
243
- <Bar dataKey="quantity" name="Quantity" fill="#6366f1" radius={[0, 4, 4, 0]} />
243
+ <Tooltip formatter={(v, name) => (name === t("reports.quantity") ? [v, name] : [money.format(v), name])} />
244
+ <Bar dataKey="quantity" name={t("reports.quantity")} fill="#6366f1" radius={[0, 4, 4, 0]} />
244
245
  </BarChart>
245
246
  </ResponsiveContainer>
246
247
  )}
@@ -251,7 +252,7 @@ function TopSellersTab({ range, setRange }) {
251
252
  columns={[
252
253
  {
253
254
  key: "name",
254
- label: "Product",
255
+ label: t("reports.product"),
255
256
  render: (r) =>
256
257
  r.product_id ? (
257
258
  <Link to={href(`products/${r.product_id}`)} className="font-medium hover:underline">
@@ -261,12 +262,12 @@ function TopSellersTab({ range, setRange }) {
261
262
  r.name
262
263
  ),
263
264
  },
264
- { key: "quantity", label: "Quantity sold" },
265
- { key: "total", label: "Net revenue", render: (r) => money.format(r.total) },
265
+ { key: "quantity", label: t("reports.quantity_sold") },
266
+ { key: "total", label: t("reports.net_revenue"), render: (r) => money.format(r.total) },
266
267
  ]}
267
268
  rows={rows}
268
269
  loading={loading}
269
- empty={{ icon: BarChart3, title: "No sales in this period" }}
270
+ empty={{ icon: BarChart3, title: t("reports.empty_no_sales") }}
270
271
  />
271
272
  </div>
272
273
  );
@@ -359,30 +360,30 @@ function TotalsTab() {
359
360
  return (
360
361
  <div className="grid grid-cols-1 gap-4 sm:grid-cols-2 xl:grid-cols-4">
361
362
  <TotalsCard
362
- title="Orders by status"
363
+ title={t("reports.orders_by_status")}
363
364
  loading={loading}
364
365
  rows={normalizeTotals(d["orders-totals"])}
365
366
  renderLabel={(r) => <StatusBadge status={r.key} className="text-[10px]" />}
366
367
  />
367
368
  <TotalsCard
368
- title="Products by status"
369
+ title={t("reports.products_by_status")}
369
370
  loading={loading}
370
371
  rows={normalizeTotals(d["products-totals"], PRODUCT_STATUSES)}
371
372
  />
372
- <TotalsCard title="Customers" loading={loading} rows={normalizeTotals(d["customers-totals"])} />
373
+ <TotalsCard title={t("routes.customers")} loading={loading} rows={normalizeTotals(d["customers-totals"])} />
373
374
  <TotalsCard
374
- title="Coupons by type"
375
+ title={t("reports.coupons_by_type")}
375
376
  loading={loading}
376
377
  rows={normalizeTotals(d["coupons-totals"], COUPON_TYPES)}
377
378
  />
378
379
  <TotalsCard
379
- title="Reviews by status"
380
+ title={t("reports.reviews_by_status")}
380
381
  loading={loading}
381
382
  rows={normalizeTotals(d["reviews-totals"], REVIEW_STATUSES)}
382
383
  />
383
- <TotalsCard title="Categories" loading={loading} rows={normalizeTotals(d["categories-totals"])} />
384
- <TotalsCard title="Ribbons" loading={loading} rows={normalizeTotals(d["ribbons-totals"])} />
385
- <TotalsCard title="Attributes" loading={loading} rows={normalizeTotals(d["attributes-totals"])} />
384
+ <TotalsCard title={t("routes.categories")} loading={loading} rows={normalizeTotals(d["categories-totals"])} />
385
+ <TotalsCard title={t("reports.ribbons")} loading={loading} rows={normalizeTotals(d["ribbons-totals"])} />
386
+ <TotalsCard title={t("reports.attributes")} loading={loading} rows={normalizeTotals(d["attributes-totals"])} />
386
387
  </div>
387
388
  );
388
389
  }
@@ -394,12 +395,12 @@ export default function Reports() {
394
395
 
395
396
  return (
396
397
  <div>
397
- <PageHeader title="Reports" description="Sales performance and store totals." />
398
+ <PageHeader title={t("routes.reports")} description={t("reports.subtitle")} />
398
399
  <Tabs defaultValue="sales">
399
400
  <TabsList className="mb-4">
400
- <TabsTrigger value="sales">Sales</TabsTrigger>
401
- <TabsTrigger value="top-sellers">Top sellers</TabsTrigger>
402
- <TabsTrigger value="totals">Totals</TabsTrigger>
401
+ <TabsTrigger value="sales">{t("reports.tab_sales")}</TabsTrigger>
402
+ <TabsTrigger value="top-sellers">{t("reports.tab_top_sellers")}</TabsTrigger>
403
+ <TabsTrigger value="totals">{t("reports.tab_totals")}</TabsTrigger>
403
404
  </TabsList>
404
405
  <TabsContent value="sales">
405
406
  <SalesTab range={range} setRange={setRange} />
@@ -24,6 +24,7 @@ import {
24
24
  import { Settings2 } from "lucide-react";
25
25
  import { EMAIL_TYPES } from "../../lib/constants";
26
26
  import { call } from "../../lib/api";
27
+ import { t } from "../../i18n";
27
28
  import useGroupForm from "./useGroupForm";
28
29
 
29
30
  const DEFAULTS = {
@@ -69,13 +70,13 @@ export default function EmailsSettings() {
69
70
  typeCfg(e.id).recipient ||
70
71
  recipientsText ||
71
72
  effective?.recipients.join(", ") ||
72
- "Admin";
73
+ t("settings.emails.admin");
73
74
 
74
75
  // One list for every email; a delimiter row splits the admin-targeted
75
76
  // emails from the ones the customer receives.
76
77
  const GROUPS = [
77
- { label: "Sent to the store admin", types: EMAIL_TYPES.filter((e) => e.recipient === "admin") },
78
- { label: "Sent to the customer", types: EMAIL_TYPES.filter((e) => e.recipient === "customer") },
78
+ { label: t("settings.emails.sent_to_admin"), types: EMAIL_TYPES.filter((e) => e.recipient === "admin") },
79
+ { label: t("settings.emails.sent_to_customer"), types: EMAIL_TYPES.filter((e) => e.recipient === "customer") },
79
80
  ];
80
81
 
81
82
  const active = activeId ? EMAIL_TYPES.find((e) => e.id === activeId) : null;
@@ -85,42 +86,39 @@ export default function EmailsSettings() {
85
86
  <Card>
86
87
  <CardHeader className="flex-row items-start justify-between gap-4 space-y-0">
87
88
  <div className="space-y-1.5">
88
- <CardTitle>Sender options</CardTitle>
89
+ <CardTitle>{t("settings.emails.sender_options")}</CardTitle>
89
90
  <CardDescription>
90
- How transactional emails are named, and who gets the admin copies. The sending
91
- address is controlled by your Base44 email configuration, not here.
91
+ {t("settings.emails.sender_options_description")}
92
92
  </CardDescription>
93
93
  </div>
94
94
  <SaveButton />
95
95
  </CardHeader>
96
96
  <CardContent className="grid gap-4 sm:grid-cols-2">
97
97
  <div className="space-y-1.5 sm:col-span-2">
98
- <Label>Store name</Label>
98
+ <Label>{t("setup.store_name")}</Label>
99
99
  <Input
100
100
  value={form.store_name || ""}
101
- placeholder="Your store's name"
101
+ placeholder={t("setup.store_name_placeholder")}
102
102
  onChange={(e) => setField("store_name", e.target.value)}
103
103
  />
104
104
  <p className="text-xs text-muted-foreground">
105
- Used in the subject line and as the “From” name — e.g.{" "}
106
- “[{storeName || "Your store"}]: New order #1002”, sent from {storeName || "Your store"}.
107
- Leave it blank to send as your app's name.
105
+ {t("settings.emails.store_name_hint", { name: storeName || t("settings.emails.your_store") })}
108
106
  </p>
109
107
  </div>
110
108
  <div className="space-y-1.5 sm:col-span-2">
111
- <Label>Admin notification recipients</Label>
109
+ <Label>{t("settings.emails.admin_recipients")}</Label>
112
110
  <Input
113
111
  value={recipientsText}
114
- placeholder={adminUsers || "admin@example.com, ops@example.com"}
112
+ placeholder={adminUsers || t("settings.emails.recipients_placeholder")}
115
113
  onChange={(e) => setRecipients(e.target.value)}
116
114
  />
117
115
  <p className="text-xs text-muted-foreground">
118
- Comma-separated; receives new/cancelled/failed order emails. Leave blank to use{" "}
119
- {adminUsers ? <strong>{adminUsers}</strong> : "your app's admin users"} — the app's
120
- users with the admin role.
116
+ {t("settings.emails.recipients_hint", {
117
+ users: adminUsers || t("settings.emails.admin_users_fallback"),
118
+ })}
121
119
  {effective && !adminUsers && (
122
120
  <span className="block">
123
- No app user has the admin role, so leaving this blank sends these emails nowhere.
121
+ {t("settings.emails.no_admin_warning")}
124
122
  </span>
125
123
  )}
126
124
  </p>
@@ -131,10 +129,9 @@ export default function EmailsSettings() {
131
129
  <Card>
132
130
  <CardHeader className="flex-row items-start justify-between gap-4 space-y-0">
133
131
  <div className="space-y-1.5">
134
- <CardTitle>Email notifications</CardTitle>
132
+ <CardTitle>{t("settings.emails.notifications")}</CardTitle>
135
133
  <CardDescription>
136
- Enable, disable and customize each transactional email, including the
137
- low-stock and out-of-stock alerts.
134
+ {t("settings.emails.notifications_description")}
138
135
  </CardDescription>
139
136
  </div>
140
137
  <SaveButton />
@@ -143,9 +140,9 @@ export default function EmailsSettings() {
143
140
  <Table>
144
141
  <TableHeader>
145
142
  <TableRow>
146
- <TableHead>Email</TableHead>
147
- <TableHead>Recipient</TableHead>
148
- <TableHead className="w-24">Enabled</TableHead>
143
+ <TableHead>{t("common.email")}</TableHead>
144
+ <TableHead>{t("settings.emails.recipient")}</TableHead>
145
+ <TableHead className="w-24">{t("settings.emails.enabled")}</TableHead>
149
146
  <TableHead className="w-24" />
150
147
  </TableRow>
151
148
  </TableHeader>
@@ -167,10 +164,10 @@ export default function EmailsSettings() {
167
164
  <TableCell>
168
165
  <div className="flex items-center gap-2">
169
166
  <span className="font-medium">{e.label}</span>
170
- {e.manual && <Badge variant="outline" className="font-normal">Manual</Badge>}
167
+ {e.manual && <Badge variant="outline" className="font-normal">{t("settings.emails.manual")}</Badge>}
171
168
  {managed && (
172
169
  <Badge variant="secondary" className="font-normal text-muted-foreground">
173
- Handled by Base44 auth
170
+ {t("settings.emails.managed_by_auth")}
174
171
  </Badge>
175
172
  )}
176
173
  </div>
@@ -180,7 +177,7 @@ export default function EmailsSettings() {
180
177
  ? "—"
181
178
  : e.recipient === "admin"
182
179
  ? adminRecipient(e)
183
- : "Customer"}
180
+ : t("routes.customer")}
184
181
  </TableCell>
185
182
  <TableCell>
186
183
  {managed ? (
@@ -193,7 +190,7 @@ export default function EmailsSettings() {
193
190
  {!managed && (
194
191
  <Button variant="ghost" size="sm" onClick={() => setActiveId(e.id)}>
195
192
  <Settings2 className="mr-2 h-4 w-4" />
196
- Manage
193
+ {t("settings.emails.manage")}
197
194
  </Button>
198
195
  )}
199
196
  </TableCell>
@@ -211,57 +208,57 @@ export default function EmailsSettings() {
211
208
  <Dialog open onOpenChange={(o) => !o && setActiveId(null)}>
212
209
  <DialogContent>
213
210
  <DialogHeader>
214
- <DialogTitle>{active.label} email</DialogTitle>
211
+ <DialogTitle>{t("settings.emails.type_email_title", { type: active.label })}</DialogTitle>
215
212
  </DialogHeader>
216
213
  <div className="space-y-4">
217
214
  {active.recipient === "admin" && (
218
215
  <div className="space-y-1.5">
219
- <Label>Recipient(s)</Label>
216
+ <Label>{t("settings.emails.recipients_label")}</Label>
220
217
  <Input
221
- placeholder="Leave blank to use the default admin recipients"
218
+ placeholder={t("settings.emails.recipient_placeholder")}
222
219
  value={typeCfg(active.id).recipient || ""}
223
220
  onChange={(e) => setTypeField(active.id, "recipient", e.target.value)}
224
221
  />
225
222
  <p className="text-xs text-muted-foreground">
226
- Comma-separated. Overrides the admin recipients above for this email only.
223
+ {t("settings.emails.recipient_override_hint")}
227
224
  </p>
228
225
  </div>
229
226
  )}
230
227
  {!active.stock && (
231
228
  <>
232
229
  <div className="space-y-1.5">
233
- <Label>Subject</Label>
230
+ <Label>{t("settings.emails.subject")}</Label>
234
231
  <Input
235
- placeholder="Leave blank for the default"
232
+ placeholder={t("settings.emails.default_placeholder")}
236
233
  value={typeCfg(active.id).subject || ""}
237
234
  onChange={(e) => setTypeField(active.id, "subject", e.target.value)}
238
235
  />
239
236
  </div>
240
237
  <div className="space-y-1.5">
241
- <Label>Email heading</Label>
238
+ <Label>{t("settings.emails.heading")}</Label>
242
239
  <Input
243
- placeholder="Leave blank for the default"
240
+ placeholder={t("settings.emails.default_placeholder")}
244
241
  value={typeCfg(active.id).heading || ""}
245
242
  onChange={(e) => setTypeField(active.id, "heading", e.target.value)}
246
243
  />
247
244
  </div>
248
245
  <div className="space-y-1.5">
249
- <Label>Additional content</Label>
246
+ <Label>{t("settings.emails.additional_content")}</Label>
250
247
  <Textarea
251
248
  rows={3}
252
- placeholder="Text appended below the email body."
249
+ placeholder={t("settings.emails.additional_content_placeholder")}
253
250
  value={typeCfg(active.id).additional_content || ""}
254
251
  onChange={(e) => setTypeField(active.id, "additional_content", e.target.value)}
255
252
  />
256
253
  </div>
257
254
  <p className="text-xs text-muted-foreground">
258
- Placeholders: {"{order_number}"}, {"{customer_name}"}, {"{store_name}"}, {"{order_total}"}.
255
+ {t("settings.emails.placeholders_hint")}
259
256
  </p>
260
257
  </>
261
258
  )}
262
259
  </div>
263
260
  <DialogFooter>
264
- <Button onClick={() => setActiveId(null)}>Done</Button>
261
+ <Button onClick={() => setActiveId(null)}>{t("settings.emails.done")}</Button>
265
262
  </DialogFooter>
266
263
  </DialogContent>
267
264
  </Dialog>
@@ -10,6 +10,7 @@ import {
10
10
  SelectValue,
11
11
  } from "@/components/ui/select";
12
12
  import { CURRENCIES, WEIGHT_UNITS, DIMENSION_UNITS } from "../../lib/constants";
13
+ import { t, currencyName } from "../../i18n";
13
14
  import useGroupForm from "./useGroupForm";
14
15
 
15
16
  const DEFAULTS = {
@@ -27,22 +28,21 @@ export default function GeneralSettings() {
27
28
  <Card>
28
29
  <CardHeader className="flex-row items-start justify-between gap-4 space-y-0">
29
30
  <div className="space-y-1.5">
30
- <CardTitle>Checkout</CardTitle>
31
- <CardDescription>Where customers land once they come back from paying.</CardDescription>
31
+ <CardTitle>{t("settings.general.checkout")}</CardTitle>
32
+ <CardDescription>{t("settings.general.checkout_description")}</CardDescription>
32
33
  </div>
33
34
  <SaveButton />
34
35
  </CardHeader>
35
36
  <CardContent>
36
37
  <div className="space-y-1.5">
37
- <Label>Payment return path</Label>
38
+ <Label>{t("settings.general.payment_return_path")}</Label>
38
39
  <Input
39
40
  placeholder="/order-received"
40
41
  value={form.order_received_path || ""}
41
42
  onChange={(e) => setField("order_received_path", e.target.value)}
42
43
  />
43
44
  <p className="text-xs text-muted-foreground">
44
- The storefront route customers come back to after paying, on your storefront's own
45
- address. It must match the page you built — a wrong path sends paying customers to a 404.
45
+ {t("settings.general.payment_return_path_hint")}
46
46
  </p>
47
47
  </div>
48
48
  </CardContent>
@@ -51,17 +51,16 @@ export default function GeneralSettings() {
51
51
  <Card>
52
52
  <CardHeader className="flex-row items-start justify-between gap-4 space-y-0">
53
53
  <div className="space-y-1.5">
54
- <CardTitle>Currency</CardTitle>
54
+ <CardTitle>{t("settings.general.currency")}</CardTitle>
55
55
  <CardDescription>
56
- The store's currency. Prices are formatted per the viewer's locale
57
- (Intl) — there is nothing else to configure.
56
+ {t("settings.general.currency_description")}
58
57
  </CardDescription>
59
58
  </div>
60
59
  <SaveButton />
61
60
  </CardHeader>
62
61
  <CardContent>
63
62
  <div className="max-w-xs space-y-1.5">
64
- <Label>Currency</Label>
63
+ <Label>{t("settings.general.currency")}</Label>
65
64
  <Select value={form.currency} onValueChange={(v) => setField("currency", v)}>
66
65
  <SelectTrigger>
67
66
  <SelectValue />
@@ -69,7 +68,7 @@ export default function GeneralSettings() {
69
68
  <SelectContent>
70
69
  {CURRENCIES.map((c) => (
71
70
  <SelectItem key={c.code} value={c.code}>
72
- {c.name} ({c.symbol})
71
+ {currencyName(c.code, c.name)} ({c.symbol})
73
72
  </SelectItem>
74
73
  ))}
75
74
  </SelectContent>
@@ -81,14 +80,14 @@ export default function GeneralSettings() {
81
80
  <Card>
82
81
  <CardHeader className="flex-row items-start justify-between gap-4 space-y-0">
83
82
  <div className="space-y-1.5">
84
- <CardTitle>Measurements</CardTitle>
85
- <CardDescription>Units used for product weight and dimensions.</CardDescription>
83
+ <CardTitle>{t("settings.general.measurements")}</CardTitle>
84
+ <CardDescription>{t("settings.general.measurements_description")}</CardDescription>
86
85
  </div>
87
86
  <SaveButton />
88
87
  </CardHeader>
89
88
  <CardContent className="grid gap-4 sm:grid-cols-2">
90
89
  <div className="space-y-1.5">
91
- <Label>Weight unit</Label>
90
+ <Label>{t("settings.general.weight_unit")}</Label>
92
91
  <Select value={form.weight_unit} onValueChange={(v) => setField("weight_unit", v)}>
93
92
  <SelectTrigger>
94
93
  <SelectValue />
@@ -103,7 +102,7 @@ export default function GeneralSettings() {
103
102
  </Select>
104
103
  </div>
105
104
  <div className="space-y-1.5">
106
- <Label>Dimensions unit</Label>
105
+ <Label>{t("settings.general.dimension_unit")}</Label>
107
106
  <Select value={form.dimension_unit} onValueChange={(v) => setField("dimension_unit", v)}>
108
107
  <SelectTrigger>
109
108
  <SelectValue />
@@ -3,6 +3,7 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/com
3
3
  import { Input } from "@/components/ui/input";
4
4
  import { Label } from "@/components/ui/label";
5
5
  import { Switch } from "@/components/ui/switch";
6
+ import { t } from "../../i18n";
6
7
  import useGroupForm from "./useGroupForm";
7
8
 
8
9
  const DEFAULTS = {
@@ -25,22 +26,22 @@ export default function InventorySettings() {
25
26
  <Card>
26
27
  <CardHeader className="flex-row items-start justify-between gap-4 space-y-0">
27
28
  <div className="space-y-1.5">
28
- <CardTitle>Inventory</CardTitle>
29
- <CardDescription>Stock management defaults for products that track inventory.</CardDescription>
29
+ <CardTitle>{t("routes.settings_inventory")}</CardTitle>
30
+ <CardDescription>{t("settings.inventory.description")}</CardDescription>
30
31
  </div>
31
32
  <SaveButton />
32
33
  </CardHeader>
33
34
  <CardContent className="space-y-4">
34
35
  <div className="flex items-start justify-between gap-4">
35
36
  <div>
36
- <Label>Manage stock</Label>
37
- <p className="text-xs text-muted-foreground">Enable stock management store-wide.</p>
37
+ <Label>{t("settings.inventory.manage_stock")}</Label>
38
+ <p className="text-xs text-muted-foreground">{t("settings.inventory.manage_stock_hint")}</p>
38
39
  </div>
39
40
  <Switch checked={!!form.manage_stock} onCheckedChange={(v) => setField("manage_stock", v)} />
40
41
  </div>
41
42
 
42
43
  <div className="space-y-1.5">
43
- <Label className={off ? "opacity-50" : ""}>Hold stock (minutes)</Label>
44
+ <Label className={off ? "opacity-50" : ""}>{t("settings.inventory.hold_stock")}</Label>
44
45
  <Input
45
46
  type="number"
46
47
  min={0}
@@ -49,15 +50,13 @@ export default function InventorySettings() {
49
50
  onChange={num("hold_stock_minutes", 60)}
50
51
  />
51
52
  <p className="text-xs text-muted-foreground">
52
- Hold stock for unpaid orders for this many minutes. When the limit is reached, the
53
- pending order is cancelled and its stock released. Release runs opportunistically —
54
- see the commerce skill's references/operations.md.
53
+ {t("settings.inventory.hold_stock_hint")}
55
54
  </p>
56
55
  </div>
57
56
 
58
57
  <div className="grid gap-4 sm:grid-cols-2">
59
58
  <div className="space-y-1.5">
60
- <Label className={off ? "opacity-50" : ""}>Low stock threshold</Label>
59
+ <Label className={off ? "opacity-50" : ""}>{t("settings.inventory.low_stock_threshold")}</Label>
61
60
  <Input
62
61
  type="number"
63
62
  min={0}
@@ -67,7 +66,7 @@ export default function InventorySettings() {
67
66
  />
68
67
  </div>
69
68
  <div className="space-y-1.5">
70
- <Label className={off ? "opacity-50" : ""}>Out of stock threshold</Label>
69
+ <Label className={off ? "opacity-50" : ""}>{t("settings.inventory.out_of_stock_threshold")}</Label>
71
70
  <Input
72
71
  type="number"
73
72
  disabled={off}
@@ -82,16 +81,16 @@ export default function InventorySettings() {
82
81
  <Card>
83
82
  <CardHeader className="flex-row items-start justify-between gap-4 space-y-0">
84
83
  <div className="space-y-1.5">
85
- <CardTitle>Visibility</CardTitle>
84
+ <CardTitle>{t("settings.inventory.visibility")}</CardTitle>
86
85
  </div>
87
86
  <SaveButton />
88
87
  </CardHeader>
89
88
  <CardContent>
90
89
  <div className="flex items-start justify-between gap-4">
91
90
  <div>
92
- <Label>Hide out of stock items from the catalog</Label>
91
+ <Label>{t("settings.inventory.hide_out_of_stock")}</Label>
93
92
  <p className="text-xs text-muted-foreground">
94
- Storefront APIs will exclude out-of-stock products from listings.
93
+ {t("settings.inventory.hide_out_of_stock_hint")}
95
94
  </p>
96
95
  </div>
97
96
  <Switch