@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.
- package/README.md +4 -10
- package/base44/entities/commerce.ProductReview.jsonc +2 -2
- package/base44/functions/commerce/storefront-catalog/entry.ts +7 -3
- package/package.json +1 -1
- package/scripts/install.js +2 -4
- package/skills/commerce/SKILL.md +9 -3
- package/skills/commerce/docs/api-storefront.md +3 -3
- package/skills/commerce/install/01-install.md +8 -24
- package/skills/commerce/install/02-storefront.md +21 -9
- package/skills/commerce/references/admin-localization.md +95 -0
- package/skills/commerce/references/reviews.md +7 -6
- package/src/commerce/admin/README.md +36 -14
- package/src/commerce/admin/bot/StoreAdminBot.jsx +12 -11
- package/src/commerce/admin/components/AddressForm.jsx +13 -12
- package/src/commerce/admin/components/ConfirmDialog.jsx +5 -4
- package/src/commerce/admin/components/CountrySelect.jsx +12 -8
- package/src/commerce/admin/components/DataTable.jsx +3 -2
- package/src/commerce/admin/components/DateRangePicker.jsx +11 -10
- package/src/commerce/admin/components/EmptyState.jsx +2 -1
- package/src/commerce/admin/components/MediaUploader.jsx +3 -2
- package/src/commerce/admin/components/MetaDataEditor.jsx +4 -3
- package/src/commerce/admin/components/SearchSelect.jsx +4 -3
- package/src/commerce/admin/context/SettingsContext.jsx +12 -25
- package/src/commerce/admin/hooks/useMoney.js +3 -2
- package/src/commerce/admin/i18n/index.js +54 -0
- package/src/commerce/admin/i18n/locales/de.js +896 -0
- package/src/commerce/admin/i18n/locales/en.js +900 -0
- package/src/commerce/admin/i18n/locales/es.js +896 -0
- package/src/commerce/admin/i18n/locales/fr.js +896 -0
- package/src/commerce/admin/i18n/locales/ja.js +896 -0
- package/src/commerce/admin/i18n/locales/pt.js +896 -0
- package/src/commerce/admin/index.jsx +6 -30
- package/src/commerce/admin/layout/AccessDenied.jsx +8 -9
- package/src/commerce/admin/layout/AuthGuard.jsx +2 -1
- package/src/commerce/admin/layout/Sidebar.jsx +29 -18
- package/src/commerce/admin/layout/Topbar.jsx +4 -3
- package/src/commerce/admin/lib/api.js +3 -2
- package/src/commerce/admin/lib/constants.js +44 -43
- package/src/commerce/admin/lib/format.js +4 -3
- package/src/commerce/admin/lib/paths.js +2 -3
- package/src/commerce/admin/pages/Dashboard.jsx +25 -24
- package/src/commerce/admin/pages/coupons/CouponEditor.jsx +55 -57
- package/src/commerce/admin/pages/coupons/CouponsList.jsx +22 -21
- package/src/commerce/admin/pages/customers/CustomerEditor.jsx +31 -30
- package/src/commerce/admin/pages/customers/CustomersList.jsx +19 -18
- package/src/commerce/admin/pages/orders/OrderEditor.jsx +75 -71
- package/src/commerce/admin/pages/orders/OrdersList.jsx +16 -15
- package/src/commerce/admin/pages/orders/components/AddProductDialog.jsx +12 -11
- package/src/commerce/admin/pages/orders/components/DownloadPermissionsPanel.jsx +14 -13
- package/src/commerce/admin/pages/orders/components/LineItemsTable.jsx +12 -11
- package/src/commerce/admin/pages/orders/components/OrderNotesPanel.jsx +11 -10
- package/src/commerce/admin/pages/orders/components/PaymentPanel.jsx +22 -22
- package/src/commerce/admin/pages/orders/components/RefundPanel.jsx +18 -17
- package/src/commerce/admin/pages/orders/components/TotalsBox.jsx +18 -17
- package/src/commerce/admin/pages/products/Categories.jsx +14 -13
- package/src/commerce/admin/pages/products/ProductEditor.jsx +16 -15
- package/src/commerce/admin/pages/products/ProductsList.jsx +41 -36
- package/src/commerce/admin/pages/products/Reviews.jsx +54 -46
- package/src/commerce/admin/pages/products/components/AttributesSection.jsx +45 -39
- package/src/commerce/admin/pages/products/components/ProductDataPanel.jsx +8 -7
- package/src/commerce/admin/pages/products/components/PublishBox.jsx +10 -9
- package/src/commerce/admin/pages/products/components/TaxonomyPanel.jsx +18 -17
- package/src/commerce/admin/pages/products/components/tabs/DownloadsTab.jsx +11 -10
- package/src/commerce/admin/pages/products/components/tabs/LinkedTab.jsx +9 -10
- package/src/commerce/admin/pages/products/components/tabs/ModifiersTab.jsx +3 -9
- package/src/commerce/admin/pages/products/components/tabs/PriceInventoryTab.jsx +76 -78
- package/src/commerce/admin/pages/reports/Reports.jsx +33 -32
- package/src/commerce/admin/pages/settings/EmailsSettings.jsx +36 -39
- package/src/commerce/admin/pages/settings/GeneralSettings.jsx +13 -14
- package/src/commerce/admin/pages/settings/InventorySettings.jsx +12 -13
- package/src/commerce/admin/pages/settings/LocationEditor.jsx +48 -47
- package/src/commerce/admin/pages/settings/PaymentsSettings.jsx +32 -33
- package/src/commerce/admin/pages/settings/SettingsLayout.jsx +13 -12
- package/src/commerce/admin/pages/settings/ShippingTaxSettings.jsx +31 -31
- package/src/commerce/admin/pages/settings/useGroupForm.jsx +4 -3
- package/src/commerce/admin/pages/status/WebhookEditor.jsx +34 -31
- package/src/commerce/admin/pages/status/Webhooks.jsx +11 -10
- package/src/commerce/admin/routes.jsx +31 -43
- package/src/commerce/storefront/pickers.jsx +19 -5
- package/src/commerce/storefront/useCheckout.jsx +28 -3
- 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
|
-
"
|
|
18
|
-
"
|
|
19
|
-
"
|
|
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 || "
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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="
|
|
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("
|
|
39
|
-
{field("
|
|
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("
|
|
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("
|
|
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("
|
|
48
|
+
{field(t("address.line2"), <Input value={value.address_2 || ""} onChange={setInput("address_2")} disabled={disabled} />)}
|
|
48
49
|
</div>
|
|
49
|
-
{field("
|
|
50
|
-
{field("
|
|
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
|
-
"
|
|
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
|
-
"
|
|
57
|
+
t("address.state"),
|
|
57
58
|
states ? (
|
|
58
59
|
<Select value={value.state || ""} onValueChange={set("state")} disabled={disabled}>
|
|
59
60
|
<SelectTrigger>
|
|
60
|
-
<SelectValue placeholder="
|
|
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("
|
|
75
|
-
{showPhone && field("
|
|
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 = "
|
|
22
|
-
description = "
|
|
23
|
-
confirmLabel = "
|
|
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}>
|
|
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 = "
|
|
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">
|
|
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="
|
|
50
|
+
<CommandInput placeholder={t("country.search")} />
|
|
47
51
|
<CommandList>
|
|
48
|
-
<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
|
-
|
|
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: "
|
|
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>
|
|
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
|
|
13
|
-
const daysAgo = (n) => new Date(
|
|
13
|
+
const day = startOfDay(today);
|
|
14
|
+
const daysAgo = (n) => new Date(day.getFullYear(), day.getMonth(), day.getDate() - n);
|
|
14
15
|
return [
|
|
15
|
-
{ label: "
|
|
16
|
-
{ label: "
|
|
17
|
-
{ label: "
|
|
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: "
|
|
20
|
-
from: new Date(
|
|
21
|
-
to: new Date(
|
|
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: "
|
|
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
|
-
: "
|
|
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 = "
|
|
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 ? "
|
|
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 || "
|
|
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="
|
|
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="
|
|
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" />
|
|
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 = "
|
|
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
|
-
?
|
|
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>
|
|
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("
|
|
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>
|
|
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">
|
|
147
|
+
<Label htmlFor="store-name">{t("setup.store_name")}</Label>
|
|
151
148
|
<Input
|
|
152
149
|
id="store-name"
|
|
153
150
|
value={storeName}
|
|
154
|
-
placeholder="
|
|
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
|
-
|
|
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>
|
|
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
|
-
|
|
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
|
|
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(
|
|
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
|
+
}
|