@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
@@ -1,5 +1,4 @@
1
1
  import React from "react";
2
- import { useOutlet } from "react-router-dom";
3
2
  import { Toaster } from "sonner";
4
3
  import AuthGuard from "./layout/AuthGuard";
5
4
  import AdminLayout from "./layout/AdminLayout";
@@ -7,50 +6,27 @@ import AdminRoutes from "./routes";
7
6
  import { SettingsProvider } from "./context/SettingsContext";
8
7
  import { BasePathProvider } from "./context/BasePathContext";
9
8
 
10
- export { default as AdminRoutes } from "./routes";
11
-
12
9
  /**
13
10
  * The store admin application.
14
11
  *
15
- * Mount it as a *layout route* in the app's own `src/App.jsx`: declare the few
16
- * screens that should be listed among the app's pages there, as literal `<Route>`
17
- * JSX, and hand everything deeper to `<AdminRoutes />` on a splat. Base44
18
- * discovers an app's pages by reading that file rather than running it, so a
19
- * route declared anywhere else is reachable by URL but never listed — and a
20
- * splat is skipped by discovery, which is what keeps the editors and the
21
- * settings tabs off the list while leaving them navigable.
22
- *
23
- * <Route path="/store-admin" element={<AdminApp />}>
24
- * <Route index element={<Dashboard />} />
25
- * <Route path="orders" element={<OrdersList />} />
26
- * …
27
- * <Route path="*" element={<AdminRoutes />} />
28
- * </Route>
12
+ * Mount inside your app's router:
13
+ * <Route path="/store-admin/*" element={<AdminApp />} />
29
14
  *
30
15
  * If mounted somewhere other than /store-admin, pass the prefix:
31
- * <Route path="/backoffice" element={<AdminApp basePath="/backoffice" />}>
32
- *
33
- * The providers belong here, above the outlet, and must not be repeated per
34
- * route: SettingsProvider fetches commerce.StoreSettings on mount, so wrapping
35
- * each route would cost a round trip and a remounted sidebar on every admin
36
- * navigation.
16
+ * <Route path="/backoffice/*" element={<AdminApp basePath="/backoffice" />} />
37
17
  *
38
18
  * Requires an authenticated user with role "admin" (enforced by AuthGuard,
39
19
  * and independently by entity RLS + requireAdmin() in backend functions).
40
20
  */
41
21
  export default function AdminApp({ basePath = "/store-admin" }) {
42
- // Installs from before the routes moved into App.jsx mount the whole admin
43
- // behind one splat route (`<Route path="/store-admin/*" element={<AdminApp />} />`)
44
- // and declare no children, so nothing ever fills the outlet. Fall back to the
45
- // kit's own router there rather than drawing an empty page area under the
46
- // sidebar; those apps keep working until an agent rewrites their App.jsx.
47
- const outlet = useOutlet();
48
22
  return (
49
23
  <BasePathProvider value={basePath}>
50
24
  <Toaster richColors position="top-right" />
51
25
  <AuthGuard>
52
26
  <SettingsProvider>
53
- <AdminLayout>{outlet ?? <AdminRoutes />}</AdminLayout>
27
+ <AdminLayout>
28
+ <AdminRoutes />
29
+ </AdminLayout>
54
30
  </SettingsProvider>
55
31
  </AuthGuard>
56
32
  </BasePathProvider>
@@ -2,6 +2,7 @@ import React from "react";
2
2
  import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
3
3
  import { Button } from "@/components/ui/button";
4
4
  import { Lock, ShieldAlert } from "lucide-react";
5
+ import { t } from "../i18n";
5
6
 
6
7
  /**
7
8
  * Access screens for the admin.
@@ -21,29 +22,27 @@ export default function AccessDenied({ variant = "forbidden", email, onLogin, on
21
22
  <ShieldAlert className="h-6 w-6 text-destructive" />
22
23
  )}
23
24
  </div>
24
- <CardTitle>{unauth ? "Sign in required" : "Admin access required"}</CardTitle>
25
+ <CardTitle>{unauth ? t("auth.sign_in_required") : t("auth.admin_required")}</CardTitle>
25
26
  <CardDescription>
26
27
  {unauth
27
28
  ? timedOut
28
- ? "We couldn't confirm who you are. Sign in to continue — if this page is embedded in a preview pane, open it in its own browser tab first, since sign-in can't complete inside a frame."
29
- : "You must sign in to access the store admin."
30
- : "Your account does not have the admin role. Ask a store administrator to grant you access."}
29
+ ? t("auth.sign_in_timeout")
30
+ : t("auth.must_sign_in")
31
+ : t("auth.no_admin_role")}
31
32
  </CardDescription>
32
33
  </CardHeader>
33
34
  <CardContent className="space-y-3">
34
35
  {unauth ? (
35
36
  <Button onClick={onLogin} className="w-full">
36
- Sign in
37
+ {t("auth.sign_in")}
37
38
  </Button>
38
39
  ) : (
39
40
  <>
40
41
  {email && (
41
- <p className="text-sm text-muted-foreground">
42
- Signed in as <span className="font-medium">{email}</span>
43
- </p>
42
+ <p className="text-sm text-muted-foreground">{t("auth.signed_in_as", { email })}</p>
44
43
  )}
45
44
  <Button variant="outline" onClick={onLogout} className="w-full">
46
- Sign out
45
+ {t("auth.sign_out")}
47
46
  </Button>
48
47
  </>
49
48
  )}
@@ -1,6 +1,7 @@
1
1
  import React, { createContext, useContext, useEffect, useState } from "react";
2
2
  import { Loader2 } from "lucide-react";
3
3
  import { base44 } from "../lib/api";
4
+ import { t } from "../i18n";
4
5
  import AccessDenied from "./AccessDenied";
5
6
 
6
7
  const AuthContext = createContext(null);
@@ -55,7 +56,7 @@ export default function AuthGuard({ children }) {
55
56
  return (
56
57
  <div className="flex h-screen flex-col items-center justify-center gap-3">
57
58
  <Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
58
- <p className="text-xs text-muted-foreground">Checking your access…</p>
59
+ <p className="text-xs text-muted-foreground">{t("auth.checking_access")}</p>
59
60
  </div>
60
61
  );
61
62
  }
@@ -1,5 +1,5 @@
1
1
  import React, { useEffect, useRef, useState } from "react";
2
- import { NavLink, useLocation } from "react-router-dom";
2
+ import { Link, NavLink, useLocation } from "react-router-dom";
3
3
  import { Badge } from "@/components/ui/badge";
4
4
  import {
5
5
  BadgePercent,
@@ -9,36 +9,38 @@ import {
9
9
  Settings,
10
10
  ShoppingCart,
11
11
  Sparkles,
12
+ Store,
12
13
  Users,
13
14
  } from "lucide-react";
14
15
  import { call } from "../lib/api";
15
16
  import { useAdminHref } from "../context/BasePathContext";
17
+ import { t } from "../i18n";
16
18
 
17
19
  const NAV = [
18
- { items: [{ label: "Dashboard", path: "", icon: LayoutDashboard, end: true }] },
20
+ { items: [{ label: t("routes.dashboard"), path: "", icon: LayoutDashboard, end: true }] },
19
21
  {
20
22
  items: [
21
- { label: "Orders", path: "orders", icon: ShoppingCart, badge: "processing" },
22
- { label: "Customers", path: "customers", icon: Users },
23
- { label: "Reports", path: "reports", icon: BarChart3 },
23
+ { label: t("routes.orders"), path: "orders", icon: ShoppingCart, badge: "processing" },
24
+ { label: t("routes.customers"), path: "customers", icon: Users },
25
+ { label: t("routes.reports"), path: "reports", icon: BarChart3 },
24
26
  ],
25
27
  },
26
28
  {
27
- title: "Products",
29
+ title: t("nav.products_group"),
28
30
  icon: Package,
29
31
  items: [
30
- { label: "All Products", path: "products", end: true },
31
- { label: "Categories", path: "products/categories" },
32
- { label: "Reviews", path: "products/reviews" },
32
+ { label: t("nav.all_products"), path: "products", end: true },
33
+ { label: t("routes.categories"), path: "products/categories" },
34
+ { label: t("routes.reviews"), path: "products/reviews" },
33
35
  ],
34
36
  },
35
37
  {
36
- title: "Marketing",
38
+ title: t("nav.marketing_group"),
37
39
  icon: BadgePercent,
38
- items: [{ label: "Coupons", path: "coupons" }],
40
+ items: [{ label: t("routes.coupons"), path: "coupons" }],
39
41
  },
40
42
  {
41
- items: [{ label: "Settings", path: "settings", icon: Settings }],
43
+ items: [{ label: t("routes.settings"), path: "settings", icon: Settings }],
42
44
  },
43
45
  ];
44
46
 
@@ -70,7 +72,7 @@ export default function Sidebar({ onNavigate, onOpenBot }) {
70
72
  <div className="flex h-full flex-col">
71
73
  <div className="flex h-14 items-center gap-2 border-b px-4">
72
74
  <Package className="h-5 w-5 text-primary" />
73
- <span className="font-semibold">Store Management</span>
75
+ <span className="font-semibold">{t("nav.store_management")}</span>
74
76
  </div>
75
77
  <nav className="flex-1 space-y-4 overflow-y-auto p-3">
76
78
  {NAV.map((group, gi) => (
@@ -105,18 +107,27 @@ export default function Sidebar({ onNavigate, onOpenBot }) {
105
107
  </div>
106
108
  ))}
107
109
  </nav>
108
- {onOpenBot && (
109
- <div className="border-t p-3">
110
+ <div className="space-y-0.5 border-t p-3">
111
+ {/* The way back out — the storefront the merchant is managing. */}
112
+ <Link
113
+ to="/"
114
+ onClick={onNavigate}
115
+ className="flex items-center gap-2.5 rounded-md px-3 py-2 text-sm text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
116
+ >
117
+ <Store className="h-4 w-4 shrink-0" />
118
+ <span className="flex-1">{t("nav.view_storefront")}</span>
119
+ </Link>
120
+ {onOpenBot && (
110
121
  <button
111
122
  type="button"
112
123
  onClick={onOpenBot}
113
124
  className="flex w-full items-center gap-2.5 rounded-md px-3 py-2 text-sm text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
114
125
  >
115
126
  <Sparkles className="h-4 w-4 shrink-0 text-primary" />
116
- <span className="flex-1 text-left">StoreAdmin bot</span>
127
+ <span className="flex-1 text-left">{t("nav.storeadmin_bot")}</span>
117
128
  </button>
118
- </div>
119
- )}
129
+ )}
130
+ </div>
120
131
  </div>
121
132
  );
122
133
  }
@@ -11,6 +11,7 @@ import {
11
11
  } from "@/components/ui/dropdown-menu";
12
12
  import { ChevronRight, LogOut, Menu, UserCircle } from "lucide-react";
13
13
  import { base44 } from "../lib/api";
14
+ import { t } from "../i18n";
14
15
  import { useAuth } from "./AuthGuard";
15
16
  import { useBasePath, useAdminHref } from "../context/BasePathContext";
16
17
  import { ADMIN_ROUTES } from "../routes";
@@ -30,7 +31,7 @@ function useBreadcrumbs() {
30
31
  const base = useBasePath();
31
32
  const rel = pathname.startsWith(base) ? pathname.slice(base.length) : pathname;
32
33
  const segs = rel.split("/").filter(Boolean);
33
- const crumbs = [{ label: "Dashboard", path: "" }];
34
+ const crumbs = [{ label: t("routes.dashboard"), path: "" }];
34
35
  segs.forEach((seg, i) => {
35
36
  const prefix = segs.slice(0, i + 1).join("/");
36
37
  const route = matchRoute(prefix);
@@ -80,12 +81,12 @@ export default function Topbar({ onMenuClick }) {
80
81
  </DropdownMenuTrigger>
81
82
  <DropdownMenuContent align="end">
82
83
  <DropdownMenuLabel className="font-normal">
83
- <div className="text-sm font-medium">{user?.full_name || "Administrator"}</div>
84
+ <div className="text-sm font-medium">{user?.full_name || t("auth.administrator")}</div>
84
85
  <div className="text-xs text-muted-foreground">{user?.email}</div>
85
86
  </DropdownMenuLabel>
86
87
  <DropdownMenuSeparator />
87
88
  <DropdownMenuItem onClick={() => base44.auth.logout()}>
88
- <LogOut className="mr-2 h-4 w-4" /> Sign out
89
+ <LogOut className="mr-2 h-4 w-4" /> {t("auth.sign_out")}
89
90
  </DropdownMenuItem>
90
91
  </DropdownMenuContent>
91
92
  </DropdownMenu>
@@ -9,6 +9,7 @@
9
9
  */
10
10
  import { base44 } from "@/api/base44Client";
11
11
  import { toast } from "sonner";
12
+ import { t } from "../i18n";
12
13
 
13
14
  /**
14
15
  * Invoke a backend function using the template's `{action, ...payload}` convention.
@@ -31,7 +32,7 @@ export async function call(fn, action, payload = {}, opts = {}) {
31
32
  const data = res?.data;
32
33
  if (data && typeof data === "object" && "success" in data) {
33
34
  if (data.success === false) {
34
- throw Object.assign(new Error(data.error || "Request failed"), {
35
+ throw Object.assign(new Error(data.error || t("common.request_failed")), {
35
36
  code: data.code,
36
37
  details: data,
37
38
  });
@@ -41,7 +42,7 @@ export async function call(fn, action, payload = {}, opts = {}) {
41
42
  return data;
42
43
  } catch (err) {
43
44
  const body = err.response?.data;
44
- const message = body?.error || err.message || "Request failed";
45
+ const message = body?.error || err.message || t("common.request_failed");
45
46
  const wrapped = Object.assign(new Error(message), {
46
47
  code: body?.code || err.code,
47
48
  details: body || err.details || null,
@@ -1,56 +1,57 @@
1
- /** Shared enums, labels and badge colors for the admin UI. */
1
+ /** Shared enums, labels and badge colors for the admin UI. Labels come from i18n. */
2
+ import { t } from "../i18n";
2
3
 
3
4
  export const ORDER_STATUSES = [
4
- { value: "pending", label: "Pending payment", color: "bg-amber-100 text-amber-800 border-amber-200" },
5
- { value: "processing", label: "Processing", color: "bg-green-100 text-green-800 border-green-200" },
6
- { value: "on-hold", label: "On hold", color: "bg-orange-100 text-orange-800 border-orange-200" },
7
- { value: "completed", label: "Completed", color: "bg-blue-100 text-blue-800 border-blue-200" },
8
- { value: "cancelled", label: "Cancelled", color: "bg-gray-100 text-gray-700 border-gray-200" },
9
- { value: "refunded", label: "Refunded", color: "bg-slate-100 text-slate-700 border-slate-200" },
10
- { value: "failed", label: "Failed", color: "bg-red-100 text-red-800 border-red-200" },
5
+ { value: "pending", label: t("status.order.pending"), color: "bg-amber-100 text-amber-800 border-amber-200" },
6
+ { value: "processing", label: t("status.order.processing"), color: "bg-green-100 text-green-800 border-green-200" },
7
+ { value: "on-hold", label: t("status.order.on-hold"), color: "bg-orange-100 text-orange-800 border-orange-200" },
8
+ { value: "completed", label: t("status.order.completed"), color: "bg-blue-100 text-blue-800 border-blue-200" },
9
+ { value: "cancelled", label: t("status.order.cancelled"), color: "bg-gray-100 text-gray-700 border-gray-200" },
10
+ { value: "refunded", label: t("status.order.refunded"), color: "bg-slate-100 text-slate-700 border-slate-200" },
11
+ { value: "failed", label: t("status.order.failed"), color: "bg-red-100 text-red-800 border-red-200" },
11
12
  ];
12
13
 
13
14
  export const PRODUCT_STATUSES = [
14
- { value: "draft", label: "Draft", color: "bg-gray-100 text-gray-700 border-gray-200" },
15
- { value: "pending", label: "Pending review", color: "bg-amber-100 text-amber-800 border-amber-200" },
16
- { value: "private", label: "Private", color: "bg-slate-100 text-slate-700 border-slate-200" },
17
- { value: "publish", label: "Published", color: "bg-green-100 text-green-800 border-green-200" },
15
+ { value: "draft", label: t("status.product.draft"), color: "bg-gray-100 text-gray-700 border-gray-200" },
16
+ { value: "pending", label: t("status.product.pending"), color: "bg-amber-100 text-amber-800 border-amber-200" },
17
+ { value: "private", label: t("status.product.private"), color: "bg-slate-100 text-slate-700 border-slate-200" },
18
+ { value: "publish", label: t("status.product.publish"), color: "bg-green-100 text-green-800 border-green-200" },
18
19
  ];
19
20
 
20
21
  export const STOCK_STATUSES = [
21
- { value: "instock", label: "In stock", color: "bg-green-100 text-green-800 border-green-200" },
22
- { value: "outofstock", label: "Out of stock", color: "bg-red-100 text-red-800 border-red-200" },
23
- { value: "onbackorder", label: "On backorder", color: "bg-amber-100 text-amber-800 border-amber-200" },
22
+ { value: "instock", label: t("status.stock.instock"), color: "bg-green-100 text-green-800 border-green-200" },
23
+ { value: "outofstock", label: t("status.stock.outofstock"), color: "bg-red-100 text-red-800 border-red-200" },
24
+ { value: "onbackorder", label: t("status.stock.onbackorder"), color: "bg-amber-100 text-amber-800 border-amber-200" },
24
25
  ];
25
26
 
26
27
  export const BACKORDER_OPTIONS = [
27
- { value: "no", label: "Do not allow" },
28
- { value: "notify", label: "Allow, but notify customer" },
29
- { value: "yes", label: "Allow" },
28
+ { value: "no", label: t("backorder.no") },
29
+ { value: "notify", label: t("backorder.notify") },
30
+ { value: "yes", label: t("backorder.yes") },
30
31
  ];
31
32
 
32
33
  export const TAX_STATUSES = [
33
- { value: "taxable", label: "Taxable" },
34
- { value: "none", label: "None" },
34
+ { value: "taxable", label: t("tax_status.taxable") },
35
+ { value: "none", label: t("tax_status.none") },
35
36
  ];
36
37
 
37
38
  export const COUPON_TYPES = [
38
- { value: "percent", label: "Percentage discount" },
39
- { value: "fixed_cart", label: "Fixed cart discount" },
40
- { value: "fixed_product", label: "Fixed product discount" },
39
+ { value: "percent", label: t("coupon_type.percent") },
40
+ { value: "fixed_cart", label: t("coupon_type.fixed_cart") },
41
+ { value: "fixed_product", label: t("coupon_type.fixed_product") },
41
42
  ];
42
43
 
43
44
  export const REVIEW_STATUSES = [
44
- { value: "approved", label: "Approved", color: "bg-green-100 text-green-800 border-green-200" },
45
- { value: "hold", label: "Pending", color: "bg-amber-100 text-amber-800 border-amber-200" },
46
- { value: "spam", label: "Spam", color: "bg-red-100 text-red-800 border-red-200" },
47
- { value: "trash", label: "Trash", color: "bg-gray-100 text-gray-700 border-gray-200" },
45
+ { value: "approved", label: t("status.review.approved"), color: "bg-green-100 text-green-800 border-green-200" },
46
+ { value: "hold", label: t("status.review.hold"), color: "bg-amber-100 text-amber-800 border-amber-200" },
47
+ { value: "spam", label: t("status.review.spam"), color: "bg-red-100 text-red-800 border-red-200" },
48
+ { value: "trash", label: t("status.review.trash"), color: "bg-gray-100 text-gray-700 border-gray-200" },
48
49
  ];
49
50
 
50
51
  export const WEBHOOK_STATUSES = [
51
- { value: "active", label: "Active", color: "bg-green-100 text-green-800 border-green-200" },
52
- { value: "paused", label: "Paused", color: "bg-amber-100 text-amber-800 border-amber-200" },
53
- { value: "disabled", label: "Disabled", color: "bg-gray-100 text-gray-700 border-gray-200" },
52
+ { value: "active", label: t("status.webhook.active"), color: "bg-green-100 text-green-800 border-green-200" },
53
+ { value: "paused", label: t("status.webhook.paused"), color: "bg-amber-100 text-amber-800 border-amber-200" },
54
+ { value: "disabled", label: t("status.webhook.disabled"), color: "bg-gray-100 text-gray-700 border-gray-200" },
54
55
  ];
55
56
 
56
57
  export const WEBHOOK_TOPICS = [
@@ -66,19 +67,19 @@ export const WEBHOOK_TOPICS = [
66
67
  * `managed_by_auth` emails are handled by Base44 auth, not this template.
67
68
  */
68
69
  export const EMAIL_TYPES = [
69
- { id: "new_order", label: "New order", recipient: "admin" },
70
- { id: "cancelled_order", label: "Cancelled order", recipient: "admin" },
71
- { id: "failed_order", label: "Failed order", recipient: "admin" },
72
- { id: "low_stock", label: "Low stock", recipient: "admin", stock: true },
73
- { id: "out_of_stock", label: "Out of stock", recipient: "admin", stock: true },
74
- { id: "on_hold_order", label: "Order on-hold", recipient: "customer" },
75
- { id: "processing_order", label: "Processing order", recipient: "customer" },
76
- { id: "completed_order", label: "Completed order", recipient: "customer" },
77
- { id: "refunded_order", label: "Refunded order", recipient: "customer" },
78
- { id: "customer_invoice", label: "Customer invoice / Order details", recipient: "customer", manual: true },
79
- { id: "customer_note", label: "Customer note", recipient: "customer" },
80
- { id: "reset_password", label: "Reset password", recipient: "customer", managed_by_auth: true },
81
- { id: "new_account", label: "New account", recipient: "customer", managed_by_auth: true },
70
+ { id: "new_order", label: t("email_type.new_order"), recipient: "admin" },
71
+ { id: "cancelled_order", label: t("email_type.cancelled_order"), recipient: "admin" },
72
+ { id: "failed_order", label: t("email_type.failed_order"), recipient: "admin" },
73
+ { id: "low_stock", label: t("email_type.low_stock"), recipient: "admin", stock: true },
74
+ { id: "out_of_stock", label: t("email_type.out_of_stock"), recipient: "admin", stock: true },
75
+ { id: "on_hold_order", label: t("email_type.on_hold_order"), recipient: "customer" },
76
+ { id: "processing_order", label: t("email_type.processing_order"), recipient: "customer" },
77
+ { id: "completed_order", label: t("email_type.completed_order"), recipient: "customer" },
78
+ { id: "refunded_order", label: t("email_type.refunded_order"), recipient: "customer" },
79
+ { id: "customer_invoice", label: t("email_type.customer_invoice"), recipient: "customer", manual: true },
80
+ { id: "customer_note", label: t("email_type.customer_note"), recipient: "customer" },
81
+ { id: "reset_password", label: t("email_type.reset_password"), recipient: "customer", managed_by_auth: true },
82
+ { id: "new_account", label: t("email_type.new_account"), recipient: "customer", managed_by_auth: true },
82
83
  ];
83
84
 
84
85
  export const CURRENCIES = [
@@ -1,17 +1,18 @@
1
- /** Date/text formatting helpers. */
1
+ /** Date/text formatting helpers. Dates render in the admin's i18n locale. */
2
+ import { locale } from "../i18n";
2
3
 
3
4
  export function formatDate(iso) {
4
5
  if (!iso) return "—";
5
6
  const d = new Date(iso);
6
7
  if (isNaN(d)) return "—";
7
- return d.toLocaleDateString(undefined, { year: "numeric", month: "short", day: "numeric" });
8
+ return d.toLocaleDateString(locale, { year: "numeric", month: "short", day: "numeric" });
8
9
  }
9
10
 
10
11
  export function formatDateTime(iso) {
11
12
  if (!iso) return "—";
12
13
  const d = new Date(iso);
13
14
  if (isNaN(d)) return "—";
14
- return d.toLocaleString(undefined, {
15
+ return d.toLocaleString(locale, {
15
16
  year: "numeric",
16
17
  month: "short",
17
18
  day: "numeric",
@@ -1,9 +1,8 @@
1
1
  /**
2
2
  * Mount-path handling for the admin app.
3
3
  *
4
- * Installs from before the routes moved into App.jsx mount the admin with a splat
5
- * route `<Route path="/store-admin/*">` and both halves of that pattern leak
6
- * into places they shouldn't: the `basePath` prop
4
+ * The admin is mounted with a splat route `<Route path="/store-admin/*">` and both
5
+ * halves of that pattern leak into places they shouldn't: the `basePath` prop
7
6
  * gets the pattern pasted in verbatim, and the literal URL `/store-admin/*` gets
8
7
  * opened (pattern copied into the address bar, or a nav link built from the
9
8
  * route table). Neither is a real page, so both are normalized here rather than
@@ -15,6 +15,7 @@ import {
15
15
  import { BarChart3, PackagePlus, Plus, ShoppingCart, TicketPercent, TrendingUp, AlertTriangle } from "lucide-react";
16
16
 
17
17
  import { call, base44 } from "../lib/api";
18
+ import { t } from "../i18n";
18
19
  import useAsync from "../hooks/useAsync";
19
20
  import useRealtime from "../hooks/useRealtime";
20
21
  import useMoney from "../hooks/useMoney";
@@ -85,23 +86,23 @@ export default function Dashboard() {
85
86
  <div className="space-y-6">
86
87
  <div className="flex flex-wrap items-center justify-between gap-3">
87
88
  <div>
88
- <h1 className="text-xl font-semibold tracking-tight">Dashboard</h1>
89
- <p className="text-sm text-muted-foreground">Your store at a glance.</p>
89
+ <h1 className="text-xl font-semibold tracking-tight">{t("routes.dashboard")}</h1>
90
+ <p className="text-sm text-muted-foreground">{t("dashboard.subtitle")}</p>
90
91
  </div>
91
92
  <div className="flex gap-2">
92
93
  <Button variant="outline" size="sm" asChild>
93
94
  <Link to={href("products/new")}>
94
- <PackagePlus className="mr-1.5 h-4 w-4" /> Add product
95
+ <PackagePlus className="mr-1.5 h-4 w-4" /> {t("routes.products_new")}
95
96
  </Link>
96
97
  </Button>
97
98
  <Button variant="outline" size="sm" asChild>
98
99
  <Link to={href("coupons/new")}>
99
- <TicketPercent className="mr-1.5 h-4 w-4" /> Create coupon
100
+ <TicketPercent className="mr-1.5 h-4 w-4" /> {t("dashboard.create_coupon")}
100
101
  </Link>
101
102
  </Button>
102
103
  <Button variant="outline" size="sm" asChild>
103
104
  <Link to={href("reports")}>
104
- <BarChart3 className="mr-1.5 h-4 w-4" /> View reports
105
+ <BarChart3 className="mr-1.5 h-4 w-4" /> {t("dashboard.view_reports")}
105
106
  </Link>
106
107
  </Button>
107
108
  </div>
@@ -110,28 +111,28 @@ export default function Dashboard() {
110
111
  <div className="grid grid-cols-1 gap-4 sm:grid-cols-2 xl:grid-cols-4">
111
112
  <StatCard
112
113
  icon={TrendingUp}
113
- label="Net sales today"
114
+ label={t("dashboard.net_sales_today")}
114
115
  value={format(asMoneyNumber(s.sales_today))}
115
116
  loading={summary.loading}
116
117
  />
117
118
  <StatCard
118
119
  icon={BarChart3}
119
- label="Net sales this month"
120
+ label={t("dashboard.net_sales_month")}
120
121
  value={format(asMoneyNumber(s.sales_month))}
121
122
  loading={summary.loading}
122
123
  />
123
124
  <StatCard
124
125
  icon={ShoppingCart}
125
- label="Awaiting processing"
126
+ label={t("dashboard.awaiting_processing")}
126
127
  value={processingCount}
127
- hint="orders to fulfill"
128
+ hint={t("dashboard.orders_to_fulfill")}
128
129
  loading={summary.loading}
129
130
  />
130
131
  <StatCard
131
132
  icon={AlertTriangle}
132
- label="Low stock"
133
+ label={t("dashboard.low_stock")}
133
134
  value={s.low_stock_count ?? lowStock.length}
134
- hint="products at or below threshold"
135
+ hint={t("dashboard.low_stock_hint")}
135
136
  loading={summary.loading && stock.loading}
136
137
  />
137
138
  </div>
@@ -141,7 +142,7 @@ export default function Dashboard() {
141
142
  {ORDER_STATUSES.map((st) => (
142
143
  <Link key={st.value} to={href(`orders?status=${st.value}`)}>
143
144
  <Badge variant="outline" className={`${st.color} cursor-pointer`}>
144
- {st.label}: {ordersByStatus[st.value] || 0}
145
+ {t("dashboard.status_count", { status: st.label, count: ordersByStatus[st.value] || 0 })}
145
146
  </Badge>
146
147
  </Link>
147
148
  ))}
@@ -151,7 +152,7 @@ export default function Dashboard() {
151
152
  {/* Latest orders */}
152
153
  <Card className="xl:col-span-2">
153
154
  <CardHeader className="pb-2">
154
- <CardTitle className="text-base">Latest orders</CardTitle>
155
+ <CardTitle className="text-base">{t("dashboard.latest_orders")}</CardTitle>
155
156
  </CardHeader>
156
157
  <CardContent className="p-0">
157
158
  {latest.loading ? (
@@ -164,19 +165,19 @@ export default function Dashboard() {
164
165
  <div className="py-10">
165
166
  <EmptyState
166
167
  icon={ShoppingCart}
167
- title="No orders yet"
168
- description="Orders will appear here as soon as they come in."
168
+ title={t("dashboard.no_orders_title")}
169
+ description={t("dashboard.no_orders_description")}
169
170
  />
170
171
  </div>
171
172
  ) : (
172
173
  <Table>
173
174
  <TableHeader>
174
175
  <TableRow>
175
- <TableHead>Order</TableHead>
176
- <TableHead>Date</TableHead>
177
- <TableHead>Status</TableHead>
178
- <TableHead>Customer</TableHead>
179
- <TableHead className="text-right">Total</TableHead>
176
+ <TableHead>{t("routes.order")}</TableHead>
177
+ <TableHead>{t("dashboard.date")}</TableHead>
178
+ <TableHead>{t("common.status")}</TableHead>
179
+ <TableHead>{t("routes.customer")}</TableHead>
180
+ <TableHead className="text-right">{t("dashboard.total")}</TableHead>
180
181
  </TableRow>
181
182
  </TableHeader>
182
183
  <TableBody>
@@ -204,7 +205,7 @@ export default function Dashboard() {
204
205
  {/* Low stock */}
205
206
  <Card>
206
207
  <CardHeader className="pb-2">
207
- <CardTitle className="text-base">Low on stock</CardTitle>
208
+ <CardTitle className="text-base">{t("dashboard.low_on_stock")}</CardTitle>
208
209
  </CardHeader>
209
210
  <CardContent className="p-0">
210
211
  {stock.loading ? (
@@ -215,7 +216,7 @@ export default function Dashboard() {
215
216
  </div>
216
217
  ) : lowStock.length === 0 ? (
217
218
  <div className="py-10">
218
- <EmptyState icon={Plus} title="All stocked up" description="No products are low on stock." />
219
+ <EmptyState icon={Plus} title={t("dashboard.all_stocked_title")} description={t("dashboard.all_stocked_description")} />
219
220
  </div>
220
221
  ) : (
221
222
  <ul className="divide-y">
@@ -227,10 +228,10 @@ export default function Dashboard() {
227
228
  >
228
229
  <span className="min-w-0">
229
230
  <span className="block truncate text-sm font-medium">{p.name}</span>
230
- {p.sku && <span className="text-xs text-muted-foreground">SKU: {p.sku}</span>}
231
+ {p.sku && <span className="text-xs text-muted-foreground">{t("dashboard.sku", { sku: p.sku })}</span>}
231
232
  </span>
232
233
  <Badge variant="outline" className="ml-2 shrink-0 bg-amber-100 text-amber-800 border-amber-200">
233
- {p.stock_quantity ?? 0} left
234
+ {t("dashboard.n_left", { count: p.stock_quantity ?? 0 })}
234
235
  </Badge>
235
236
  </Link>
236
237
  </li>