@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
@@ -24,6 +24,7 @@ import { Skeleton } from "@/components/ui/skeleton";
24
24
  import { Copy, ExternalLink, Loader2, Mail, Pencil, Plus, RotateCw, Ticket, Trash2, Truck, UserPlus } from "lucide-react";
25
25
  import { toast } from "sonner";
26
26
 
27
+ import { t } from "../../i18n";
27
28
  import { call, base44 } from "../../lib/api";
28
29
  import useAsync from "../../hooks/useAsync";
29
30
  import useDebounce from "../../hooks/useDebounce";
@@ -183,7 +184,7 @@ export default function OrderEditor() {
183
184
  .catch((err) => {
184
185
  if (cancelled) return;
185
186
  setPriceView(null);
186
- setPreviewError(err.message || "Could not price these changes");
187
+ setPreviewError(err.message || t("orders.preview_failed_generic"));
187
188
  })
188
189
  .finally(() => !cancelled && setPreviewing(false));
189
190
  return () => {
@@ -213,7 +214,7 @@ export default function OrderEditor() {
213
214
  if (!dirty) return;
214
215
  setSaving(true);
215
216
  try {
216
- if (await persist()) toast.success("Order saved");
217
+ if (await persist()) toast.success(t("orders.saved"));
217
218
  refetchAll();
218
219
  } catch {
219
220
  /* toast handled by call() */
@@ -248,7 +249,7 @@ export default function OrderEditor() {
248
249
  "recalculate",
249
250
  async () => {
250
251
  await call("admin-orders", "recalculate", { order_id: id });
251
- toast.success(dirty ? "Order saved and totals recalculated" : "Totals recalculated");
252
+ toast.success(dirty ? t("orders.saved_recalculated") : t("orders.recalculated"));
252
253
  },
253
254
  { flush: true }
254
255
  );
@@ -256,13 +257,13 @@ export default function OrderEditor() {
256
257
  const sendInvoice = () =>
257
258
  serverOp("invoice", async () => {
258
259
  await call("admin-orders", "send-email", { order_id: id, type: "customer_invoice" });
259
- toast.success("Order details sent to customer");
260
+ toast.success(t("orders.invoice_sent"));
260
261
  });
261
262
 
262
263
  const deleteOrder = () =>
263
264
  serverOp("delete", async () => {
264
265
  await call("admin-orders", "delete", { order_id: id });
265
- toast.success("Order deleted");
266
+ toast.success(t("orders.deleted"));
266
267
  navigate(href("orders"));
267
268
  });
268
269
 
@@ -272,7 +273,7 @@ export default function OrderEditor() {
272
273
  return rows.map((c) => ({
273
274
  value: c.id,
274
275
  label: [c.first_name, c.last_name].filter(Boolean).join(" ") || c.email,
275
- meta: [c.email, c.orders_count ? `${c.orders_count} order(s)` : null].filter(Boolean).join(" · "),
276
+ meta: [c.email, c.orders_count ? t("orders.n_orders", { count: c.orders_count }) : null].filter(Boolean).join(" · "),
276
277
  record: c,
277
278
  }));
278
279
  }, []);
@@ -316,6 +317,10 @@ export default function OrderEditor() {
316
317
  return { filled, skipped };
317
318
  };
318
319
 
320
+ /** "billing", "shipping" → localized, joined for toast copy. */
321
+ const addressPartNames = (keys) =>
322
+ keys.map((k) => (k === "billing" ? t("orders.addr_billing") : t("orders.addr_shipping"))).join(t("orders.and_separator"));
323
+
319
324
  const pickCustomer = async (opt) => {
320
325
  setCustomerOpt(opt);
321
326
  patchDraft({ customer_id: opt?.value || "" });
@@ -324,11 +329,11 @@ export default function OrderEditor() {
324
329
  const record = opt.record ?? (await base44.entities["commerce.Customer"].get(opt.value).catch(() => null));
325
330
  const { filled, skipped } = await applyCustomerAddresses(record);
326
331
  if (filled.length) {
327
- toast.success(`Filled ${filled.join(" and ")} from ${opt.label}`);
332
+ toast.success(t("orders.filled_from", { parts: addressPartNames(filled), name: opt.label }));
328
333
  } else if (skipped.length) {
329
- toast.info("Kept the addresses already on this order — use “Use customer's addresses” to replace them.");
334
+ toast.info(t("orders.kept_existing_addresses"));
330
335
  } else {
331
- toast.info(`${opt.label} has no saved address — enter the details below.`);
336
+ toast.info(t("orders.no_saved_address_enter_below", { name: opt.label }));
332
337
  setEditBilling(true);
333
338
  }
334
339
  };
@@ -341,12 +346,12 @@ export default function OrderEditor() {
341
346
  const record =
342
347
  customerOpt?.record ?? (await base44.entities["commerce.Customer"].get(draft.customer_id).catch(() => null));
343
348
  if (!record) {
344
- toast.error("Could not load that customer record");
349
+ toast.error(t("orders.customer_load_failed"));
345
350
  return;
346
351
  }
347
352
  const { filled } = await applyCustomerAddresses(record, { overwrite: true });
348
- if (filled.length) toast.success(`Replaced ${filled.join(" and ")} from the customer record`);
349
- else toast.info("That customer has no saved address");
353
+ if (filled.length) toast.success(t("orders.replaced_from_customer", { parts: addressPartNames(filled) }));
354
+ else toast.info(t("orders.customer_no_saved_address"));
350
355
  } finally {
351
356
  setFillingAddresses(false);
352
357
  }
@@ -390,8 +395,8 @@ export default function OrderEditor() {
390
395
  patchDraft({ customer_id: record.id });
391
396
  toast.success(
392
397
  existing
393
- ? `${label} is already a customer — linked. Save the order to keep the link.`
394
- : `Customer ${label} created. Save the order to keep the link.`
398
+ ? t("orders.already_customer_linked", { name: label })
399
+ : t("orders.customer_created", { name: label })
395
400
  );
396
401
  } catch {
397
402
  /* toast handled by call() */
@@ -431,7 +436,7 @@ export default function OrderEditor() {
431
436
  );
432
437
  }
433
438
  if (!order.data) {
434
- return <p className="py-12 text-center text-sm text-muted-foreground">Order not found.</p>;
439
+ return <p className="py-12 text-center text-sm text-muted-foreground">{t("orders.not_found")}</p>;
435
440
  }
436
441
 
437
442
  const o = order.data;
@@ -461,11 +466,11 @@ export default function OrderEditor() {
461
466
  <PageHeader
462
467
  title={
463
468
  <span className="flex items-center gap-3">
464
- Order #{o.order_number || o.id}
469
+ {t("orders.order_number", { number: o.order_number || o.id })}
465
470
  <StatusBadge status={o.status} />
466
471
  </span>
467
472
  }
468
- description={`Placed ${formatDateTime(o.created_date)} via ${o.created_via || "checkout"}${
473
+ description={`${t("orders.placed_via", { date: formatDateTime(o.created_date), via: o.created_via || "checkout" })}${
469
474
  o.payment_method_title ? ` · ${o.payment_method_title}` : ""
470
475
  }`}
471
476
  backHref={href("orders")}
@@ -477,14 +482,14 @@ export default function OrderEditor() {
477
482
  {/* General */}
478
483
  <Card>
479
484
  <CardHeader className="pb-3">
480
- <CardTitle className="text-base">General</CardTitle>
485
+ <CardTitle className="text-base">{t("orders.general")}</CardTitle>
481
486
  </CardHeader>
482
487
  {/* items-start: without it the shorter Status cell stretches to the
483
488
  row height set by the Customer cell's helper text, and its
484
489
  dropdown drifts below the Customer one. */}
485
490
  <CardContent className="grid grid-cols-1 items-start gap-4 sm:grid-cols-2">
486
491
  <div className="grid gap-1.5">
487
- <Label className="text-xs text-muted-foreground">Status</Label>
492
+ <Label className="text-xs text-muted-foreground">{t("common.status")}</Label>
488
493
  <Select value={draft.status} onValueChange={(v) => patchDraft({ status: v })}>
489
494
  <SelectTrigger>
490
495
  <SelectValue />
@@ -499,12 +504,12 @@ export default function OrderEditor() {
499
504
  </Select>
500
505
  </div>
501
506
  <div className="grid gap-1.5">
502
- <Label className="text-xs text-muted-foreground">Customer</Label>
507
+ <Label className="text-xs text-muted-foreground">{t("routes.customer")}</Label>
503
508
  <SearchSelect
504
509
  search={searchCustomers}
505
510
  value={customerOpt}
506
511
  onChange={pickCustomer}
507
- placeholder="Guest — search customers…"
512
+ placeholder={t("orders.guest_search_customers")}
508
513
  />
509
514
 
510
515
  {draft.customer_id ? (
@@ -515,18 +520,18 @@ export default function OrderEditor() {
515
520
  className="h-7 px-2 text-xs"
516
521
  disabled={fillingAddresses}
517
522
  onClick={refillFromCustomer}
518
- title="Replace the order's billing and shipping with the customer's saved addresses"
523
+ title={t("orders.use_customer_addresses_tip")}
519
524
  >
520
525
  {fillingAddresses ? (
521
526
  <Loader2 className="mr-1 h-3.5 w-3.5 animate-spin" />
522
527
  ) : (
523
528
  <RotateCw className="mr-1 h-3.5 w-3.5" />
524
529
  )}
525
- Use customer's addresses
530
+ {t("orders.use_customer_addresses")}
526
531
  </Button>
527
532
  <Button variant="ghost" size="sm" className="h-7 px-2 text-xs" asChild>
528
533
  <Link to={href(`customers/${draft.customer_id}`)}>
529
- <ExternalLink className="mr-1 h-3.5 w-3.5" /> Open customer
534
+ <ExternalLink className="mr-1 h-3.5 w-3.5" /> {t("orders.open_customer")}
530
535
  </Link>
531
536
  </Button>
532
537
  </div>
@@ -541,8 +546,8 @@ export default function OrderEditor() {
541
546
  onClick={saveAsCustomer}
542
547
  title={
543
548
  billingEmail
544
- ? "Create a customer from the billing details below and link this order to it"
545
- : "Add a billing email below to save these details as a customer"
549
+ ? t("orders.save_as_customer_tip")
550
+ : t("orders.save_as_customer_needs_email_tip")
546
551
  }
547
552
  >
548
553
  {savingCustomer ? (
@@ -550,16 +555,15 @@ export default function OrderEditor() {
550
555
  ) : (
551
556
  <UserPlus className="mr-1 h-3.5 w-3.5" />
552
557
  )}
553
- Save as customer
558
+ {t("orders.save_as_customer")}
554
559
  </Button>
555
560
  <span className="text-xs text-muted-foreground">
556
- {billingEmail ? "Guest order" : "Guest order — needs a billing email"}
561
+ {billingEmail ? t("orders.guest_order") : t("orders.guest_order_needs_email")}
557
562
  </span>
558
563
  </div>
559
564
  ) : (
560
565
  <p className="text-xs text-muted-foreground">
561
- Guest order. Pick a customer to fill the addresses from their record, or enter the details
562
- below — the billing email is where this order's emails go.
566
+ {t("orders.guest_order_hint")}
563
567
  </p>
564
568
  )}
565
569
  </div>
@@ -569,8 +573,8 @@ export default function OrderEditor() {
569
573
  {/* Addresses */}
570
574
  <div className="grid grid-cols-1 gap-6 lg:grid-cols-2">
571
575
  {[
572
- { key: "billing", title: "Billing", edit: editBilling, setEdit: setEditBilling, showEmail: true },
573
- { key: "shipping", title: "Shipping", edit: editShipping, setEdit: setEditShipping, showEmail: false },
576
+ { key: "billing", title: t("orders.billing"), edit: editBilling, setEdit: setEditBilling, showEmail: true },
577
+ { key: "shipping", title: t("orders.shipping"), edit: editShipping, setEdit: setEditShipping, showEmail: false },
574
578
  ].map(({ key, title, edit, setEdit, showEmail }) => (
575
579
  <Card key={key}>
576
580
  <CardHeader className="flex-row items-center justify-between space-y-0 pb-3">
@@ -581,9 +585,9 @@ export default function OrderEditor() {
581
585
  variant="ghost"
582
586
  size="sm"
583
587
  onClick={() => patchDraft({ shipping: { ...draft.billing, email: undefined } })}
584
- title="Copy from billing"
588
+ title={t("orders.copy_from_billing")}
585
589
  >
586
- <Copy className="mr-1 h-3.5 w-3.5" /> From billing
590
+ <Copy className="mr-1 h-3.5 w-3.5" /> {t("orders.from_billing")}
587
591
  </Button>
588
592
  )}
589
593
  <Button variant="ghost" size="icon" className="h-7 w-7" onClick={() => setEdit(!edit)}>
@@ -609,7 +613,7 @@ export default function OrderEditor() {
609
613
  {/* Items */}
610
614
  <Card>
611
615
  <CardHeader className="pb-3">
612
- <CardTitle className="text-base">Items</CardTitle>
616
+ <CardTitle className="text-base">{t("orders.items")}</CardTitle>
613
617
  </CardHeader>
614
618
  <CardContent className="p-0">
615
619
  <LineItemsTable
@@ -622,7 +626,7 @@ export default function OrderEditor() {
622
626
  "coupon",
623
627
  async () => {
624
628
  await call("admin-orders", "remove-coupon", { order_id: id, code });
625
- toast.success(`Coupon ${code} removed`);
629
+ toast.success(t("orders.coupon_removed", { code }));
626
630
  },
627
631
  { flush: true }
628
632
  )
@@ -631,22 +635,22 @@ export default function OrderEditor() {
631
635
  {editable && (
632
636
  <div className="flex flex-wrap gap-2 border-t p-3">
633
637
  <Button variant="outline" size="sm" onClick={() => setAddProductOpen(true)}>
634
- <Plus className="mr-1 h-3.5 w-3.5" /> Add product(s)
638
+ <Plus className="mr-1 h-3.5 w-3.5" /> {t("orders.add_products")}
635
639
  </Button>
636
640
  <Button variant="outline" size="sm" onClick={() => setFeeDialog(true)}>
637
- <Plus className="mr-1 h-3.5 w-3.5" /> Add fee
641
+ <Plus className="mr-1 h-3.5 w-3.5" /> {t("orders.add_fee")}
638
642
  </Button>
639
643
  <Button variant="outline" size="sm" onClick={() => setShippingDialog(true)}>
640
- <Truck className="mr-1 h-3.5 w-3.5" /> Add shipping
644
+ <Truck className="mr-1 h-3.5 w-3.5" /> {t("orders.add_shipping")}
641
645
  </Button>
642
646
  <Button
643
647
  variant="outline"
644
648
  size="sm"
645
649
  disabled={Boolean(working)}
646
- title={dirty ? "Saves your pending changes, then applies the coupon" : undefined}
650
+ title={dirty ? t("orders.apply_coupon_dirty_tip") : undefined}
647
651
  onClick={() => setCouponDialog(true)}
648
652
  >
649
- <Ticket className="mr-1 h-3.5 w-3.5" /> Apply coupon
653
+ <Ticket className="mr-1 h-3.5 w-3.5" /> {t("orders.apply_coupon")}
650
654
  </Button>
651
655
  <Button
652
656
  variant="outline"
@@ -655,8 +659,8 @@ export default function OrderEditor() {
655
659
  disabled={Boolean(working) || saving}
656
660
  title={
657
661
  dirty
658
- ? "Saves your pending changes, then re-runs tax and totals against the current settings"
659
- : "Re-run tax and totals against the current settings"
662
+ ? t("orders.recalculate_dirty_tip")
663
+ : t("orders.recalculate_tip")
660
664
  }
661
665
  onClick={recalculate}
662
666
  >
@@ -665,7 +669,7 @@ export default function OrderEditor() {
665
669
  ) : (
666
670
  <RotateCw className="mr-1 h-3.5 w-3.5" />
667
671
  )}
668
- {dirty ? "Save & recalculate" : "Recalculate"}
672
+ {dirty ? t("orders.save_and_recalculate") : t("orders.recalculate")}
669
673
  </Button>
670
674
  </div>
671
675
  )}
@@ -674,15 +678,15 @@ export default function OrderEditor() {
674
678
  <div className="mb-2 flex items-center justify-end gap-1.5 text-xs text-muted-foreground">
675
679
  {previewStale ? (
676
680
  <>
677
- <Loader2 className="h-3 w-3 animate-spin" /> Pricing your changes…
681
+ <Loader2 className="h-3 w-3 animate-spin" /> {t("orders.pricing_changes")}
678
682
  </>
679
683
  ) : previewError ? (
680
684
  <span className="text-destructive">
681
- Could not price these changes: {previewError}
685
+ {t("orders.preview_failed", { error: previewError })}
682
686
  </span>
683
687
  ) : (
684
688
  <Badge variant="secondary" className="font-normal">
685
- Unsaved — totals below include your changes
689
+ {t("orders.unsaved_totals")}
686
690
  </Badge>
687
691
  )}
688
692
  </div>
@@ -690,7 +694,7 @@ export default function OrderEditor() {
690
694
  <TotalsBox order={totalsView} />
691
695
  <div className="mt-3 flex justify-end">
692
696
  <Button variant="outline" size="sm" onClick={() => setShowRefund((v) => !v)}>
693
- {showRefund ? "Close refund" : "Refund"}
697
+ {showRefund ? t("orders.close_refund") : t("orders.refund")}
694
698
  </Button>
695
699
  </div>
696
700
  </div>
@@ -718,7 +722,7 @@ export default function OrderEditor() {
718
722
  {/* Custom fields */}
719
723
  <Card>
720
724
  <CardHeader className="pb-3">
721
- <CardTitle className="text-base">Custom fields</CardTitle>
725
+ <CardTitle className="text-base">{t("orders.custom_fields")}</CardTitle>
722
726
  </CardHeader>
723
727
  <CardContent>
724
728
  <MetaDataEditor value={draft.meta_data || []} onChange={(v) => patchDraft({ meta_data: v })} />
@@ -730,12 +734,12 @@ export default function OrderEditor() {
730
734
  <div className="space-y-6">
731
735
  <Card>
732
736
  <CardHeader className="pb-3">
733
- <CardTitle className="text-base">Order actions</CardTitle>
737
+ <CardTitle className="text-base">{t("orders.order_actions")}</CardTitle>
734
738
  </CardHeader>
735
739
  <CardContent className="space-y-2">
736
740
  <Button className="w-full" onClick={save} disabled={!dirty || saving}>
737
741
  {saving && <Loader2 className="mr-1.5 h-4 w-4 animate-spin" />}
738
- Save order
742
+ {t("orders.save_order")}
739
743
  </Button>
740
744
  <Button
741
745
  variant="outline"
@@ -748,7 +752,7 @@ export default function OrderEditor() {
748
752
  ) : (
749
753
  <Mail className="mr-1.5 h-4 w-4" />
750
754
  )}
751
- Send order details to customer
755
+ {t("orders.send_details")}
752
756
  </Button>
753
757
  <Separator />
754
758
  <Button
@@ -756,7 +760,7 @@ export default function OrderEditor() {
756
760
  className="w-full text-destructive hover:text-destructive"
757
761
  onClick={() => setDeleteOpen(true)}
758
762
  >
759
- <Trash2 className="mr-1.5 h-4 w-4" /> Delete order
763
+ <Trash2 className="mr-1.5 h-4 w-4" /> {t("orders.delete_order")}
760
764
  </Button>
761
765
  </CardContent>
762
766
  </Card>
@@ -766,7 +770,7 @@ export default function OrderEditor() {
766
770
  {o.customer_note && (
767
771
  <Card>
768
772
  <CardHeader className="pb-3">
769
- <CardTitle className="text-base">Customer provided note</CardTitle>
773
+ <CardTitle className="text-base">{t("orders.customer_provided_note")}</CardTitle>
770
774
  </CardHeader>
771
775
  <CardContent>
772
776
  <p className="whitespace-pre-wrap rounded-md border border-blue-200 bg-blue-50 p-2.5 text-sm">
@@ -791,8 +795,8 @@ export default function OrderEditor() {
791
795
  <NameAmountDialog
792
796
  open={feeDialog}
793
797
  onOpenChange={setFeeDialog}
794
- title="Add fee"
795
- nameLabel="Fee name"
798
+ title={t("orders.add_fee")}
799
+ nameLabel={t("orders.fee_name")}
796
800
  onSubmit={({ name, amount }) =>
797
801
  patchDraft({
798
802
  fee_lines: [
@@ -806,8 +810,8 @@ export default function OrderEditor() {
806
810
  <NameAmountDialog
807
811
  open={shippingDialog}
808
812
  onOpenChange={setShippingDialog}
809
- title="Add shipping"
810
- nameLabel="Method title"
813
+ title={t("orders.add_shipping")}
814
+ nameLabel={t("orders.method_title")}
811
815
  onSubmit={({ name, amount }) =>
812
816
  patchDraft({
813
817
  shipping_lines: [
@@ -826,7 +830,7 @@ export default function OrderEditor() {
826
830
  "coupon",
827
831
  async () => {
828
832
  await call("admin-orders", "apply-coupon", { order_id: id, code });
829
- toast.success(`Coupon ${code} applied`);
833
+ toast.success(t("orders.coupon_applied", { code }));
830
834
  },
831
835
  { flush: true }
832
836
  )
@@ -836,9 +840,9 @@ export default function OrderEditor() {
836
840
  <ConfirmDialog
837
841
  open={deleteOpen}
838
842
  onOpenChange={setDeleteOpen}
839
- title={`Delete order #${o.order_number || o.id}?`}
840
- description="Only pending, cancelled or failed orders can be deleted. This cannot be undone."
841
- confirmLabel="Delete order"
843
+ title={t("orders.delete_order_title", { number: o.order_number || o.id })}
844
+ description={t("orders.delete_order_description")}
845
+ confirmLabel={t("orders.delete_order")}
842
846
  loading={working === "delete"}
843
847
  onConfirm={deleteOrder}
844
848
  />
@@ -859,7 +863,7 @@ function AddressDisplay({ address = {} }) {
859
863
  address.email,
860
864
  address.phone,
861
865
  ].filter(Boolean);
862
- if (rows.length === 0) return <p className="text-sm text-muted-foreground">No address set.</p>;
866
+ if (rows.length === 0) return <p className="text-sm text-muted-foreground">{t("orders.no_address_set")}</p>;
863
867
  return (
864
868
  <address className="text-sm not-italic leading-6 text-muted-foreground">
865
869
  {rows.map((r, i) => (
@@ -892,13 +896,13 @@ function NameAmountDialog({ open, onOpenChange, title, nameLabel, onSubmit }) {
892
896
  <Input value={name} onChange={(e) => setName(e.target.value)} />
893
897
  </div>
894
898
  <div className="grid gap-1.5">
895
- <Label>Amount</Label>
899
+ <Label>{t("common.amount")}</Label>
896
900
  <Input type="number" step="any" value={amount} onChange={(e) => setAmount(Number(e.target.value) || 0)} />
897
901
  </div>
898
902
  </div>
899
903
  <DialogFooter>
900
904
  <Button variant="outline" onClick={() => onOpenChange(false)}>
901
- Cancel
905
+ {t("common.cancel")}
902
906
  </Button>
903
907
  <Button
904
908
  disabled={!name.trim()}
@@ -907,7 +911,7 @@ function NameAmountDialog({ open, onOpenChange, title, nameLabel, onSubmit }) {
907
911
  onOpenChange(false);
908
912
  }}
909
913
  >
910
- Add
914
+ {t("common.add")}
911
915
  </Button>
912
916
  </DialogFooter>
913
917
  </DialogContent>
@@ -926,15 +930,15 @@ function CouponDialog({ open, onOpenChange, onSubmit }) {
926
930
  <Dialog open={open} onOpenChange={onOpenChange}>
927
931
  <DialogContent className="sm:max-w-sm">
928
932
  <DialogHeader>
929
- <DialogTitle>Apply coupon</DialogTitle>
933
+ <DialogTitle>{t("orders.apply_coupon")}</DialogTitle>
930
934
  </DialogHeader>
931
935
  <div className="grid gap-1.5">
932
- <Label>Coupon code</Label>
933
- <Input value={code} onChange={(e) => setCode(e.target.value)} placeholder="e.g. welcome10" />
936
+ <Label>{t("orders.coupon_code")}</Label>
937
+ <Input value={code} onChange={(e) => setCode(e.target.value)} placeholder={t("orders.coupon_code_placeholder")} />
934
938
  </div>
935
939
  <DialogFooter>
936
940
  <Button variant="outline" onClick={() => onOpenChange(false)}>
937
- Cancel
941
+ {t("common.cancel")}
938
942
  </Button>
939
943
  <Button
940
944
  disabled={!code.trim()}
@@ -943,7 +947,7 @@ function CouponDialog({ open, onOpenChange, onSubmit }) {
943
947
  onOpenChange(false);
944
948
  }}
945
949
  >
946
- Apply
950
+ {t("orders.apply")}
947
951
  </Button>
948
952
  </DialogFooter>
949
953
  </DialogContent>
@@ -12,6 +12,7 @@ import {
12
12
  import { Loader2, Plus, Search, ShoppingCart } from "lucide-react";
13
13
  import { toast } from "sonner";
14
14
 
15
+ import { t } from "../../i18n";
15
16
  import { call, base44 } from "../../lib/api";
16
17
  import useAsync from "../../hooks/useAsync";
17
18
  import usePagedList from "../../hooks/usePagedList";
@@ -27,7 +28,7 @@ import DataTable from "../../components/DataTable";
27
28
  import DateRangePicker from "../../components/DateRangePicker";
28
29
  import StatusBadge from "../../components/StatusBadge";
29
30
 
30
- const TABS = [{ value: "", label: "All" }, ...ORDER_STATUSES];
31
+ const TABS = [{ value: "", label: t("orders.all") }, ...ORDER_STATUSES];
31
32
 
32
33
  export default function OrdersList() {
33
34
  const href = useAdminHref();
@@ -90,7 +91,7 @@ export default function OrdersList() {
90
91
  setBulkBusy(true);
91
92
  try {
92
93
  await call("admin-orders", "bulk-status", { ids: selected, status: bulkStatus });
93
- toast.success(`${selected.length} order(s) updated`);
94
+ toast.success(t("orders.n_updated", { count: selected.length }));
94
95
  setSelected([]);
95
96
  setBulkStatus("");
96
97
  list.refetch();
@@ -119,16 +120,16 @@ export default function OrdersList() {
119
120
  () => [
120
121
  {
121
122
  key: "order_number",
122
- label: "Order",
123
+ label: t("routes.order"),
123
124
  render: (o) => (
124
125
  <span className="font-medium">
125
126
  #{o.order_number || o.id} <span className="font-normal text-muted-foreground">{customerName(o)}</span>
126
127
  </span>
127
128
  ),
128
129
  },
129
- { key: "created_date", label: "Date", sortable: true, render: (o) => formatDate(o.created_date) },
130
- { key: "status", label: "Status", render: (o) => <StatusBadge status={o.status} /> },
131
- { key: "total", label: "Total", sortable: true, className: "text-right", render: (o) => format(o.total || 0) },
130
+ { key: "created_date", label: t("orders.date"), sortable: true, render: (o) => formatDate(o.created_date) },
131
+ { key: "status", label: t("common.status"), render: (o) => <StatusBadge status={o.status} /> },
132
+ { key: "total", label: t("orders.total"), sortable: true, className: "text-right", render: (o) => format(o.total || 0) },
132
133
  ],
133
134
  [format]
134
135
  );
@@ -138,11 +139,11 @@ export default function OrdersList() {
138
139
  return (
139
140
  <div>
140
141
  <PageHeader
141
- title="Orders"
142
+ title={t("routes.orders")}
142
143
  actions={
143
144
  <Button onClick={addOrder} disabled={creating}>
144
145
  {creating ? <Loader2 className="mr-1.5 h-4 w-4 animate-spin" /> : <Plus className="mr-1.5 h-4 w-4" />}
145
- Add order
146
+ {t("routes.orders_new")}
146
147
  </Button>
147
148
  }
148
149
  />
@@ -176,17 +177,17 @@ export default function OrdersList() {
176
177
  <Input
177
178
  value={query}
178
179
  onChange={(e) => setQuery(e.target.value)}
179
- placeholder="Search order #, name, email…"
180
+ placeholder={t("orders.search_placeholder")}
180
181
  className="w-64 pl-8"
181
182
  />
182
183
  </div>
183
184
  <DateRangePicker value={dateRange} onChange={setDateRange} />
184
185
  {selected.length > 0 && (
185
186
  <div className="ml-auto flex items-center gap-2 rounded-md border bg-muted/50 px-2 py-1">
186
- <span className="text-sm text-muted-foreground">{selected.length} selected</span>
187
+ <span className="text-sm text-muted-foreground">{t("common.selected_count", { count: selected.length })}</span>
187
188
  <Select value={bulkStatus} onValueChange={setBulkStatus}>
188
189
  <SelectTrigger className="h-8 w-44">
189
- <SelectValue placeholder="Change status to…" />
190
+ <SelectValue placeholder={t("orders.change_status_to")} />
190
191
  </SelectTrigger>
191
192
  <SelectContent>
192
193
  {ORDER_STATUSES.map((s) => (
@@ -198,7 +199,7 @@ export default function OrdersList() {
198
199
  </Select>
199
200
  <Button size="sm" onClick={applyBulk} disabled={!bulkStatus || bulkBusy}>
200
201
  {bulkBusy && <Loader2 className="mr-1 h-3.5 w-3.5 animate-spin" />}
201
- Apply
202
+ {t("orders.apply")}
202
203
  </Button>
203
204
  </div>
204
205
  )}
@@ -214,12 +215,12 @@ export default function OrdersList() {
214
215
  selected={selected}
215
216
  onSelectChange={setSelected}
216
217
  onRowClick={(o) => navigate(href(`orders/${o.id}`))}
217
- rowActions={(o) => [{ label: "View / edit", onClick: () => navigate(href(`orders/${o.id}`)) }]}
218
+ rowActions={(o) => [{ label: t("orders.view_edit"), onClick: () => navigate(href(`orders/${o.id}`)) }]}
218
219
  pagination={{ page: list.page, hasNext: list.hasNext, onNext: list.next, onPrev: list.prev }}
219
220
  empty={{
220
221
  icon: ShoppingCart,
221
- title: "No orders found",
222
- description: searchMode ? "Try adjusting your search or date range." : "Orders will appear here as they come in.",
222
+ title: t("orders.empty_title"),
223
+ description: searchMode ? t("orders.empty_search_hint") : t("orders.empty_description"),
223
224
  }}
224
225
  />
225
226
  </div>