@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
@@ -6,6 +6,7 @@ import { Badge } from "@/components/ui/badge";
6
6
  import { Loader2, RotateCcw, Send, Sparkles, Wrench, XCircle } from "lucide-react";
7
7
  import { toast } from "sonner";
8
8
  import { base44 } from "../lib/api";
9
+ import { t } from "../i18n";
9
10
  import Markdown from "./Markdown";
10
11
 
11
12
  /** Agent identifier — base44/agents/commerce/StoreAdmin.jsonc, namespaced by its folder. */
@@ -14,9 +15,9 @@ const AGENT_NAME = "commerce/StoreAdmin";
14
15
  const CONVERSATION_KEY = "commerce.StoreAdmin.conversation_id";
15
16
 
16
17
  const SUGGESTIONS = [
17
- "Show today's sales summary",
18
- "List the 10 most recent orders",
19
- "Which products are low on stock?",
18
+ "bot.suggestion_sales_summary",
19
+ "bot.suggestion_recent_orders",
20
+ "bot.suggestion_low_stock",
20
21
  ];
21
22
 
22
23
  /** One chat message bubble (user right, assistant left with markdown + tool chips). */
@@ -152,7 +153,7 @@ export default function StoreAdminBot({ open, onOpenChange }) {
152
153
  ]);
153
154
  await base44.agents.addMessage(conv, { role: "user", content: trimmed });
154
155
  } catch (err) {
155
- toast.error(err?.response?.data?.message || err.message || "Failed to reach StoreAdmin");
156
+ toast.error(err?.response?.data?.message || err.message || t("bot.error_reach"));
156
157
  } finally {
157
158
  setSending(false);
158
159
  }
@@ -181,7 +182,7 @@ export default function StoreAdminBot({ open, onOpenChange }) {
181
182
  <div className="flex items-center justify-between">
182
183
  <SheetTitle className="flex items-center gap-2 text-base">
183
184
  <Sparkles className="h-4 w-4 text-primary" />
184
- StoreAdmin
185
+ {t("bot.title")}
185
186
  </SheetTitle>
186
187
  <Button
187
188
  variant="ghost"
@@ -191,7 +192,7 @@ export default function StoreAdminBot({ open, onOpenChange }) {
191
192
  disabled={!conversationId && messages.length === 0}
192
193
  >
193
194
  <RotateCcw className="h-3 w-3" />
194
- New chat
195
+ {t("bot.new_chat")}
195
196
  </Button>
196
197
  </div>
197
198
  </SheetHeader>
@@ -200,7 +201,7 @@ export default function StoreAdminBot({ open, onOpenChange }) {
200
201
  {visibleMessages.length === 0 && (
201
202
  <div className="mt-6 space-y-3 text-center">
202
203
  <p className="text-sm text-muted-foreground">
203
- Ask me anything about your store — I can look things up and make changes for you.
204
+ {t("bot.intro")}
204
205
  </p>
205
206
  <div className="flex flex-col items-stretch gap-1.5">
206
207
  {SUGGESTIONS.map((s) => (
@@ -209,9 +210,9 @@ export default function StoreAdminBot({ open, onOpenChange }) {
209
210
  variant="outline"
210
211
  size="sm"
211
212
  className="justify-start text-xs font-normal text-muted-foreground"
212
- onClick={() => send(s)}
213
+ onClick={() => send(t(s))}
213
214
  >
214
- {s}
215
+ {t(s)}
215
216
  </Button>
216
217
  ))}
217
218
  </div>
@@ -223,7 +224,7 @@ export default function StoreAdminBot({ open, onOpenChange }) {
223
224
  {busy && (
224
225
  <div className="flex items-center gap-2 px-1 text-xs text-muted-foreground">
225
226
  <Loader2 className="h-3 w-3 animate-spin" />
226
- Working…
227
+ {t("bot.working")}
227
228
  </div>
228
229
  )}
229
230
  </div>
@@ -234,7 +235,7 @@ export default function StoreAdminBot({ open, onOpenChange }) {
234
235
  value={input}
235
236
  onChange={(e) => setInput(e.target.value)}
236
237
  onKeyDown={onKeyDown}
237
- placeholder="e.g. Show pending orders as a table"
238
+ placeholder={t("bot.input_placeholder")}
238
239
  rows={1}
239
240
  className="max-h-32 min-h-9 resize-none text-[13px]"
240
241
  />
@@ -10,6 +10,7 @@ import {
10
10
  } from "@/components/ui/select";
11
11
  import CountrySelect from "./CountrySelect";
12
12
  import { statesFor } from "../lib/geo-data";
13
+ import { t } from "../i18n";
13
14
 
14
15
  /**
15
16
  * Billing/shipping address editor (Order billing shape).
@@ -35,29 +36,29 @@ export default function AddressForm({
35
36
 
36
37
  return (
37
38
  <div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
38
- {field("First name", <Input value={value.first_name || ""} onChange={setInput("first_name")} disabled={disabled} />)}
39
- {field("Last name", <Input value={value.last_name || ""} onChange={setInput("last_name")} disabled={disabled} />)}
39
+ {field(t("address.first_name"), <Input value={value.first_name || ""} onChange={setInput("first_name")} disabled={disabled} />)}
40
+ {field(t("address.last_name"), <Input value={value.last_name || ""} onChange={setInput("last_name")} disabled={disabled} />)}
40
41
  <div className="sm:col-span-2">
41
- {field("Company", <Input value={value.company || ""} onChange={setInput("company")} disabled={disabled} />)}
42
+ {field(t("address.company"), <Input value={value.company || ""} onChange={setInput("company")} disabled={disabled} />)}
42
43
  </div>
43
44
  <div className="sm:col-span-2">
44
- {field("Address line 1", <Input value={value.address_1 || ""} onChange={setInput("address_1")} disabled={disabled} />)}
45
+ {field(t("address.line1"), <Input value={value.address_1 || ""} onChange={setInput("address_1")} disabled={disabled} />)}
45
46
  </div>
46
47
  <div className="sm:col-span-2">
47
- {field("Address line 2", <Input value={value.address_2 || ""} onChange={setInput("address_2")} disabled={disabled} />)}
48
+ {field(t("address.line2"), <Input value={value.address_2 || ""} onChange={setInput("address_2")} disabled={disabled} />)}
48
49
  </div>
49
- {field("City", <Input value={value.city || ""} onChange={setInput("city")} disabled={disabled} />)}
50
- {field("Postcode / ZIP", <Input value={value.postcode || ""} onChange={setInput("postcode")} disabled={disabled} />)}
50
+ {field(t("address.city"), <Input value={value.city || ""} onChange={setInput("city")} disabled={disabled} />)}
51
+ {field(t("address.postcode"), <Input value={value.postcode || ""} onChange={setInput("postcode")} disabled={disabled} />)}
51
52
  {field(
52
- "Country",
53
+ t("address.country"),
53
54
  <CountrySelect value={value.country || ""} onChange={(code) => onChange({ ...value, country: code, state: "" })} disabled={disabled} />
54
55
  )}
55
56
  {field(
56
- "State / Province",
57
+ t("address.state"),
57
58
  states ? (
58
59
  <Select value={value.state || ""} onValueChange={set("state")} disabled={disabled}>
59
60
  <SelectTrigger>
60
- <SelectValue placeholder="Select state…" />
61
+ <SelectValue placeholder={t("address.select_state")} />
61
62
  </SelectTrigger>
62
63
  <SelectContent>
63
64
  {states.map((s) => (
@@ -71,8 +72,8 @@ export default function AddressForm({
71
72
  <Input value={value.state || ""} onChange={setInput("state")} disabled={disabled} />
72
73
  )
73
74
  )}
74
- {showEmail && field("Email", <Input type="email" value={value.email || ""} onChange={setInput("email")} disabled={disabled} />)}
75
- {showPhone && field("Phone", <Input value={value.phone || ""} onChange={setInput("phone")} disabled={disabled} />)}
75
+ {showEmail && field(t("common.email"), <Input type="email" value={value.email || ""} onChange={setInput("email")} disabled={disabled} />)}
76
+ {showPhone && field(t("common.phone"), <Input value={value.phone || ""} onChange={setInput("phone")} disabled={disabled} />)}
76
77
  </div>
77
78
  );
78
79
  }
@@ -10,6 +10,7 @@ import {
10
10
  AlertDialogTitle,
11
11
  } from "@/components/ui/alert-dialog";
12
12
  import { Loader2 } from "lucide-react";
13
+ import { t } from "../i18n";
13
14
 
14
15
  /**
15
16
  * Confirmation dialog for destructive actions.
@@ -18,9 +19,9 @@ import { Loader2 } from "lucide-react";
18
19
  export default function ConfirmDialog({
19
20
  open,
20
21
  onOpenChange,
21
- title = "Are you sure?",
22
- description = "This action cannot be undone.",
23
- confirmLabel = "Confirm",
22
+ title = t("confirm.title"),
23
+ description = t("confirm.description"),
24
+ confirmLabel = t("common.confirm"),
24
25
  destructive = true,
25
26
  onConfirm,
26
27
  loading = false,
@@ -33,7 +34,7 @@ export default function ConfirmDialog({
33
34
  <AlertDialogDescription>{description}</AlertDialogDescription>
34
35
  </AlertDialogHeader>
35
36
  <AlertDialogFooter>
36
- <AlertDialogCancel disabled={loading}>Cancel</AlertDialogCancel>
37
+ <AlertDialogCancel disabled={loading}>{t("common.cancel")}</AlertDialogCancel>
37
38
  <AlertDialogAction
38
39
  disabled={loading}
39
40
  className={destructive ? "bg-destructive text-destructive-foreground hover:bg-destructive/90" : ""}
@@ -11,15 +11,17 @@ import {
11
11
  import { Button } from "@/components/ui/button";
12
12
  import { Check, ChevronsUpDown } from "lucide-react";
13
13
  import { COUNTRIES } from "../lib/geo-data";
14
+ import { t, countryName } from "../i18n";
14
15
 
15
16
  /**
16
- * Searchable country picker.
17
+ * Searchable country picker. Names render in the admin's language
18
+ * (Intl.DisplayNames); the English name stays searchable.
17
19
  * Props: { value (ISO code), onChange(code), placeholder?, allowEmpty?, disabled? }
18
20
  */
19
21
  export default function CountrySelect({
20
22
  value,
21
23
  onChange,
22
- placeholder = "Select country",
24
+ placeholder = t("country.select"),
23
25
  allowEmpty = false,
24
26
  disabled = false,
25
27
  className = "",
@@ -37,15 +39,17 @@ export default function CountrySelect({
37
39
  disabled={disabled}
38
40
  className={`w-full justify-between font-normal ${className}`}
39
41
  >
40
- <span className="truncate">{selected?.name || placeholder}</span>
42
+ <span className="truncate">
43
+ {selected ? countryName(selected.code, selected.name) : placeholder}
44
+ </span>
41
45
  <ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
42
46
  </Button>
43
47
  </PopoverTrigger>
44
48
  <PopoverContent className="w-(--radix-popover-trigger-width) min-w-64 p-0" align="start">
45
49
  <Command>
46
- <CommandInput placeholder="Search countries…" />
50
+ <CommandInput placeholder={t("country.search")} />
47
51
  <CommandList>
48
- <CommandEmpty>No country found.</CommandEmpty>
52
+ <CommandEmpty>{t("country.none_found")}</CommandEmpty>
49
53
  <CommandGroup>
50
54
  {allowEmpty && (
51
55
  <CommandItem
@@ -56,20 +60,20 @@ export default function CountrySelect({
56
60
  }}
57
61
  >
58
62
  <Check className={`mr-2 h-4 w-4 ${!value ? "opacity-100" : "opacity-0"}`} />
59
- Any country
63
+ {t("country.any")}
60
64
  </CommandItem>
61
65
  )}
62
66
  {COUNTRIES.map((c) => (
63
67
  <CommandItem
64
68
  key={c.code}
65
- value={`${c.name} ${c.code}`}
69
+ value={`${countryName(c.code, c.name)} ${c.name} ${c.code}`}
66
70
  onSelect={() => {
67
71
  onChange(c.code);
68
72
  setOpen(false);
69
73
  }}
70
74
  >
71
75
  <Check className={`mr-2 h-4 w-4 ${value === c.code ? "opacity-100" : "opacity-0"}`} />
72
- {c.name}
76
+ {countryName(c.code, c.name)}
73
77
  </CommandItem>
74
78
  ))}
75
79
  </CommandGroup>
@@ -18,6 +18,7 @@ import {
18
18
  } from "@/components/ui/dropdown-menu";
19
19
  import { ArrowDown, ArrowUp, ArrowUpDown, ChevronLeft, ChevronRight, MoreHorizontal } from "lucide-react";
20
20
  import EmptyState from "./EmptyState";
21
+ import { t } from "../i18n";
21
22
 
22
23
  /**
23
24
  * Generic list table.
@@ -112,7 +113,7 @@ export default function DataTable({
112
113
  ) : rows.length === 0 ? (
113
114
  <TableRow>
114
115
  <TableCell colSpan={colCount} className="py-12">
115
- <EmptyState {...(empty || { title: "Nothing here yet" })} />
116
+ <EmptyState {...(empty || { title: t("table.empty_title") })} />
116
117
  </TableCell>
117
118
  </TableRow>
118
119
  ) : (
@@ -166,7 +167,7 @@ export default function DataTable({
166
167
  </div>
167
168
  {pagination && (
168
169
  <div className="flex items-center justify-end gap-2 border-t px-3 py-2 text-sm text-muted-foreground">
169
- <span>Page {pagination.page}</span>
170
+ <span>{t("table.page", { page: pagination.page })}</span>
170
171
  <Button
171
172
  variant="outline"
172
173
  size="icon"
@@ -3,24 +3,25 @@ import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover
3
3
  import { Button } from "@/components/ui/button";
4
4
  import { Calendar } from "@/components/ui/calendar";
5
5
  import { CalendarIcon, X } from "lucide-react";
6
+ import { t } from "../i18n";
6
7
 
7
8
  const toISO = (d) => (d ? d.toISOString().slice(0, 10) : null);
8
9
 
9
10
  function presetRanges() {
10
11
  const today = new Date();
11
12
  const startOfDay = (d) => new Date(d.getFullYear(), d.getMonth(), d.getDate());
12
- const t = startOfDay(today);
13
- const daysAgo = (n) => new Date(t.getFullYear(), t.getMonth(), t.getDate() - n);
13
+ const day = startOfDay(today);
14
+ const daysAgo = (n) => new Date(day.getFullYear(), day.getMonth(), day.getDate() - n);
14
15
  return [
15
- { label: "Today", from: t, to: t },
16
- { label: "Last 7 days", from: daysAgo(6), to: t },
17
- { label: "This month", from: new Date(t.getFullYear(), t.getMonth(), 1), to: t },
16
+ { label: t("dates.today"), from: day, to: day },
17
+ { label: t("dates.last_7_days"), from: daysAgo(6), to: day },
18
+ { label: t("dates.this_month"), from: new Date(day.getFullYear(), day.getMonth(), 1), to: day },
18
19
  {
19
- label: "Last month",
20
- from: new Date(t.getFullYear(), t.getMonth() - 1, 1),
21
- to: new Date(t.getFullYear(), t.getMonth(), 0),
20
+ label: t("dates.last_month"),
21
+ from: new Date(day.getFullYear(), day.getMonth() - 1, 1),
22
+ to: new Date(day.getFullYear(), day.getMonth(), 0),
22
23
  },
23
- { label: "This year", from: new Date(t.getFullYear(), 0, 1), to: t },
24
+ { label: t("dates.this_year"), from: new Date(day.getFullYear(), 0, 1), to: day },
24
25
  ];
25
26
  }
26
27
 
@@ -34,7 +35,7 @@ export default function DateRangePicker({ value, onChange, align = "start" }) {
34
35
 
35
36
  const label = value?.from
36
37
  ? `${value.from}${value.to && value.to !== value.from ? ` → ${value.to}` : ""}`
37
- : "All time";
38
+ : t("dates.all_time");
38
39
 
39
40
  const applyPreset = (p) => {
40
41
  onChange({ from: toISO(p.from), to: toISO(p.to) });
@@ -1,11 +1,12 @@
1
1
  import React from "react";
2
2
  import { Inbox } from "lucide-react";
3
+ import { t } from "../i18n";
3
4
 
4
5
  /**
5
6
  * Empty-list placeholder.
6
7
  * Props: { icon?: LucideIcon, title?, description?, action?: node }
7
8
  */
8
- export default function EmptyState({ icon: Icon = Inbox, title = "Nothing here yet", description, action }) {
9
+ export default function EmptyState({ icon: Icon = Inbox, title = t("table.empty_title"), description, action }) {
9
10
  return (
10
11
  <div className="flex flex-col items-center justify-center gap-2 py-8 text-center">
11
12
  <Icon className="h-8 w-8 text-muted-foreground/40" />
@@ -3,6 +3,7 @@ import { Button } from "@/components/ui/button";
3
3
  import { ArrowDown, ArrowUp, ImagePlus, Loader2, X } from "lucide-react";
4
4
  import { toast } from "sonner";
5
5
  import { base44 } from "../lib/api";
6
+ import { t } from "../i18n";
6
7
 
7
8
  /** Upload a file via the Core integration; returns the public URL. */
8
9
  export async function uploadFile(file) {
@@ -23,7 +24,7 @@ export default function MediaUploader({
23
24
  onChange,
24
25
  multiple = false,
25
26
  accept = "image/*",
26
- label = multiple ? "Add images" : "Set image",
27
+ label = multiple ? t("media.add_images") : t("media.set_image"),
27
28
  }) {
28
29
  const inputRef = useRef(null);
29
30
  const [uploading, setUploading] = useState(false);
@@ -42,7 +43,7 @@ export default function MediaUploader({
42
43
  if (multiple) onChange([...(value || []), ...uploaded]);
43
44
  else onChange(uploaded[0] || null);
44
45
  } catch (err) {
45
- toast.error(err.response?.data?.error || err.message || "Upload failed");
46
+ toast.error(err.response?.data?.error || err.message || t("common.upload_failed"));
46
47
  } finally {
47
48
  setUploading(false);
48
49
  if (inputRef.current) inputRef.current.value = "";
@@ -2,6 +2,7 @@ import React from "react";
2
2
  import { Button } from "@/components/ui/button";
3
3
  import { Input } from "@/components/ui/input";
4
4
  import { Plus, X } from "lucide-react";
5
+ import { t } from "../i18n";
5
6
 
6
7
  /**
7
8
  * Key/value editor for `meta_data` arrays.
@@ -21,13 +22,13 @@ export default function MetaDataEditor({ value = [], onChange }) {
21
22
  {(value || []).map((row, i) => (
22
23
  <div key={i} className="flex items-center gap-2">
23
24
  <Input
24
- placeholder="Key"
25
+ placeholder={t("meta.key")}
25
26
  value={row.key || ""}
26
27
  onChange={(e) => setRow(i, "key", e.target.value)}
27
28
  className="w-1/3"
28
29
  />
29
30
  <Input
30
- placeholder="Value"
31
+ placeholder={t("meta.value")}
31
32
  value={row.value || ""}
32
33
  onChange={(e) => setRow(i, "value", e.target.value)}
33
34
  className="flex-1"
@@ -38,7 +39,7 @@ export default function MetaDataEditor({ value = [], onChange }) {
38
39
  </div>
39
40
  ))}
40
41
  <Button type="button" variant="outline" size="sm" onClick={addRow}>
41
- <Plus className="mr-1 h-4 w-4" /> Add field
42
+ <Plus className="mr-1 h-4 w-4" /> {t("meta.add_field")}
42
43
  </Button>
43
44
  </div>
44
45
  );
@@ -12,6 +12,7 @@ import { Button } from "@/components/ui/button";
12
12
  import { Badge } from "@/components/ui/badge";
13
13
  import { Check, ChevronsUpDown, Loader2, X } from "lucide-react";
14
14
  import useDebounce from "../hooks/useDebounce";
15
+ import { t } from "../i18n";
15
16
 
16
17
  /**
17
18
  * Async searchable picker (single or multi).
@@ -29,7 +30,7 @@ export default function SearchSelect({
29
30
  value,
30
31
  onChange,
31
32
  multiple = false,
32
- placeholder = "Search…",
33
+ placeholder = t("common.search"),
33
34
  disabled = false,
34
35
  className = "",
35
36
  }) {
@@ -80,7 +81,7 @@ export default function SearchSelect({
80
81
  <span className="truncate text-left">
81
82
  {multiple
82
83
  ? (value || []).length
83
- ? `${value.length} selected`
84
+ ? t("common.selected_count", { count: value.length })
84
85
  : placeholder
85
86
  : value?.label || placeholder}
86
87
  </span>
@@ -97,7 +98,7 @@ export default function SearchSelect({
97
98
  </div>
98
99
  ) : (
99
100
  <>
100
- <CommandEmpty>No results.</CommandEmpty>
101
+ <CommandEmpty>{t("common.no_results")}</CommandEmpty>
101
102
  <CommandGroup>
102
103
  {options.map((opt) => (
103
104
  <CommandItem key={opt.value} value={String(opt.value)} onSelect={() => pick(opt)}>
@@ -8,6 +8,7 @@ import { Label } from "@/components/ui/label";
8
8
  import { Loader2, Store } from "lucide-react";
9
9
  import { toast } from "sonner";
10
10
  import { base44, call } from "../lib/api";
11
+ import { t } from "../i18n";
11
12
 
12
13
  const SettingsCtx = createContext(null);
13
14
 
@@ -118,7 +119,7 @@ function SetupScreen({ onDone }) {
118
119
  setSchemaErrors(null);
119
120
  try {
120
121
  await call("seed-store", null, { with_sample_data: withSample, store_name: storeName.trim() }, { silent: true });
121
- toast.success("Store defaults initialized");
122
+ toast.success(t("setup.success"));
122
123
  onDone();
123
124
  } catch (err) {
124
125
  if (err.code === "schema_incompatible") {
@@ -138,26 +139,19 @@ function SetupScreen({ onDone }) {
138
139
  <div className="mb-2 flex h-10 w-10 items-center justify-center rounded-lg bg-primary/10">
139
140
  <Store className="h-5 w-5 text-primary" />
140
141
  </div>
141
- <CardTitle>Set up your store</CardTitle>
142
- <CardDescription>
143
- This looks like a fresh installation. Initialize the store with default
144
- settings (currency, tax classes, payment methods, shipping zone and
145
- email configuration) to start using the admin.
146
- </CardDescription>
142
+ <CardTitle>{t("setup.title")}</CardTitle>
143
+ <CardDescription>{t("setup.description")}</CardDescription>
147
144
  </CardHeader>
148
145
  <CardContent className="space-y-4">
149
146
  <div className="space-y-1.5">
150
- <Label htmlFor="store-name">Store name</Label>
147
+ <Label htmlFor="store-name">{t("setup.store_name")}</Label>
151
148
  <Input
152
149
  id="store-name"
153
150
  value={storeName}
154
- placeholder="Your store's name"
151
+ placeholder={t("setup.store_name_placeholder")}
155
152
  onChange={(e) => setStoreName(e.target.value)}
156
153
  />
157
- <p className="text-xs text-muted-foreground">
158
- Used in the subject and as the sender name on every transactional email.
159
- Editable later in Settings → Emails.
160
- </p>
154
+ <p className="text-xs text-muted-foreground">{t("setup.store_name_hint")}</p>
161
155
  </div>
162
156
  {hasProducts === false && (
163
157
  <div className="flex items-start gap-2 rounded-md border p-3">
@@ -168,24 +162,17 @@ function SetupScreen({ onDone }) {
168
162
  />
169
163
  <div className="grid gap-1">
170
164
  <Label htmlFor="with-sample" className="cursor-pointer">
171
- Include sample data
165
+ {t("setup.sample_label")}
172
166
  </Label>
173
- <p className="text-xs text-muted-foreground">
174
- Adds demo categories, 10 products (three with size/colour variants, one
175
- downloadable) and coupons so you can explore the admin.
176
- </p>
167
+ <p className="text-xs text-muted-foreground">{t("setup.sample_hint")}</p>
177
168
  </div>
178
169
  </div>
179
170
  )}
180
171
  {schemaErrors && (
181
172
  <Alert variant="destructive">
182
- <AlertTitle>Entity schemas are incompatible</AlertTitle>
173
+ <AlertTitle>{t("setup.schema_error_title")}</AlertTitle>
183
174
  <AlertDescription>
184
- <p className="mb-2">
185
- The entity schemas in this app have been modified in a way that
186
- prevents seeding. Restore the template schemas or fix the fields
187
- below, then retry.
188
- </p>
175
+ <p className="mb-2">{t("setup.schema_error_description")}</p>
189
176
  <ul className="list-disc space-y-1 pl-4 text-xs">
190
177
  {schemaErrors.map((e, i) => (
191
178
  <li key={i}>
@@ -198,7 +185,7 @@ function SetupScreen({ onDone }) {
198
185
  )}
199
186
  <Button onClick={run} disabled={running || !storeName.trim()} className="w-full">
200
187
  {running && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
201
- Initialize store defaults
188
+ {t("setup.run")}
202
189
  </Button>
203
190
  </CardContent>
204
191
  </Card>
@@ -1,11 +1,12 @@
1
1
  import { useCallback, useMemo } from "react";
2
2
  import { useSettings } from "../context/SettingsContext";
3
3
  import { CURRENCIES } from "../lib/constants";
4
+ import { locale } from "../i18n";
4
5
 
5
6
  /**
6
7
  * Currency formatter driven by `general.currency`. Formatting itself is
7
8
  * localization's job — Intl.NumberFormat renders the symbol, separators and
8
- * decimals for the browser locale; there are no format settings.
9
+ * decimals for the admin's i18n locale; there are no format settings.
9
10
  * Returns { format(n), symbol, code, decimals }.
10
11
  */
11
12
  export default function useMoney() {
@@ -17,7 +18,7 @@ export default function useMoney() {
17
18
  const currency = CURRENCIES.find((cc) => cc.code === code);
18
19
  let formatter = null;
19
20
  try {
20
- formatter = new Intl.NumberFormat(undefined, { style: "currency", currency: code });
21
+ formatter = new Intl.NumberFormat(locale, { style: "currency", currency: code });
21
22
  } catch {
22
23
  formatter = null; // unknown code — fall back below
23
24
  }
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Admin UI localization. Every user-visible string in the admin renders
3
+ * through `t()`; the wording lives in `./locales/<lang>.js` (en, de, es, fr,
4
+ * ja, pt ship with the plugin; en is the default and the fallback).
5
+ *
6
+ * Switching language is a one-line code change: point the `active` import
7
+ * below at another locale file. There is deliberately no runtime picker — a
8
+ * store's back office runs in one language.
9
+ *
10
+ * Adding a language: copy `./locales/en.js` to `./locales/<code>.js`, set its
11
+ * `$locale` tag, translate every value (keep `{placeholders}` verbatim), then
12
+ * point the `active` import at the new file. Missing keys fall back to
13
+ * English, so a partial file renders mixed rather than broken.
14
+ */
15
+ import en from "./locales/en.js";
16
+ import active from "./locales/en.js"; // ← the admin's language: "./locales/de.js", "./locales/es.js", "./locales/fr.js", "./locales/ja.js", "./locales/pt.js"
17
+
18
+ /** BCP-47 tag of the active language — drives Intl date/number/name formatting. */
19
+ export const locale = active.$locale || "en";
20
+
21
+ /**
22
+ * Message lookup with `{name}` interpolation: t("x.y", { name: "Ada" }).
23
+ * Falls back to English, then to the key itself, so an untranslated or
24
+ * missing key renders readable text instead of throwing.
25
+ */
26
+ export function t(key, vars) {
27
+ let s = active[key] ?? en[key] ?? key;
28
+ if (vars) {
29
+ for (const k of Object.keys(vars)) s = s.split(`{${k}}`).join(String(vars[k] ?? ""));
30
+ }
31
+ return s;
32
+ }
33
+
34
+ let regionNames;
35
+ /** Country name in the active language via Intl.DisplayNames; `fallback` is the English name. */
36
+ export function countryName(code, fallback) {
37
+ try {
38
+ regionNames ||= new Intl.DisplayNames([locale], { type: "region" });
39
+ return regionNames.of(code) || fallback || code;
40
+ } catch {
41
+ return fallback || code;
42
+ }
43
+ }
44
+
45
+ let currencyNames;
46
+ /** Currency name in the active language via Intl.DisplayNames; `fallback` is the English name. */
47
+ export function currencyName(code, fallback) {
48
+ try {
49
+ currencyNames ||= new Intl.DisplayNames([locale], { type: "currency" });
50
+ return currencyNames.of(code) || fallback || code;
51
+ } catch {
52
+ return fallback || code;
53
+ }
54
+ }