@nextclaw/ui 0.5.9 → 0.5.11
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/CHANGELOG.md +12 -0
- package/dist/assets/index-BGzsyzDd.css +1 -0
- package/dist/assets/index-BHB8zYn7.js +342 -0
- package/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/components/common/MaskedInput.tsx +1 -1
- package/src/components/common/StatusBadge.tsx +3 -5
- package/src/components/common/TagInput.tsx +3 -2
- package/src/components/config/ChannelForm.tsx +106 -104
- package/src/components/config/ChannelsList.tsx +22 -23
- package/src/components/config/CronConfig.tsx +20 -30
- package/src/components/config/ModelConfig.tsx +11 -12
- package/src/components/config/ProviderForm.tsx +3 -3
- package/src/components/config/ProvidersList.tsx +13 -13
- package/src/components/config/RuntimeConfig.tsx +40 -43
- package/src/components/config/SessionsConfig.tsx +12 -24
- package/src/components/layout/Sidebar.tsx +70 -40
- package/src/components/layout/page-layout.tsx +71 -0
- package/src/components/marketplace/MarketplacePage.tsx +34 -35
- package/src/components/providers/I18nProvider.tsx +64 -0
- package/src/components/ui/confirm-dialog.tsx +3 -2
- package/src/components/ui/tabs-custom.tsx +2 -1
- package/src/hooks/useConfirmDialog.tsx +5 -4
- package/src/hooks/useMarketplace.ts +4 -3
- package/src/lib/i18n.ts +267 -5
- package/src/main.tsx +6 -3
- package/dist/assets/index-BtwwwWcv.css +0 -1
- package/dist/assets/index-STUSj6p9.js +0 -337
|
@@ -1,49 +1,61 @@
|
|
|
1
1
|
import { cn } from '@/lib/utils';
|
|
2
|
-
import { t } from '@/lib/i18n';
|
|
3
|
-
import { Cpu, GitBranch, History, MessageSquare, Sparkles, BookOpen, Store, AlarmClock } from 'lucide-react';
|
|
2
|
+
import { LANGUAGE_OPTIONS, t, type I18nLanguage } from '@/lib/i18n';
|
|
3
|
+
import { Cpu, GitBranch, History, MessageSquare, Sparkles, BookOpen, Store, AlarmClock, Languages } from 'lucide-react';
|
|
4
4
|
import { NavLink } from 'react-router-dom';
|
|
5
5
|
import { useDocBrowser } from '@/components/doc-browser';
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
{
|
|
9
|
-
target: '/model',
|
|
10
|
-
label: 'Models',
|
|
11
|
-
icon: Cpu,
|
|
12
|
-
},
|
|
13
|
-
{
|
|
14
|
-
target: '/providers',
|
|
15
|
-
label: 'Providers',
|
|
16
|
-
icon: Sparkles,
|
|
17
|
-
},
|
|
18
|
-
{
|
|
19
|
-
target: '/channels',
|
|
20
|
-
label: 'Channels',
|
|
21
|
-
icon: MessageSquare,
|
|
22
|
-
},
|
|
23
|
-
{
|
|
24
|
-
target: '/runtime',
|
|
25
|
-
label: 'Routing & Runtime',
|
|
26
|
-
icon: GitBranch,
|
|
27
|
-
},
|
|
28
|
-
{
|
|
29
|
-
target: '/sessions',
|
|
30
|
-
label: t('sessions'),
|
|
31
|
-
icon: History,
|
|
32
|
-
},
|
|
33
|
-
{
|
|
34
|
-
target: '/cron',
|
|
35
|
-
label: t('cron'),
|
|
36
|
-
icon: AlarmClock,
|
|
37
|
-
},
|
|
38
|
-
{
|
|
39
|
-
target: '/marketplace',
|
|
40
|
-
label: 'Marketplace',
|
|
41
|
-
icon: Store,
|
|
42
|
-
}
|
|
43
|
-
];
|
|
6
|
+
import { useI18n } from '@/components/providers/I18nProvider';
|
|
7
|
+
import { Select, SelectContent, SelectItem, SelectTrigger } from '@/components/ui/select';
|
|
44
8
|
|
|
45
9
|
export function Sidebar() {
|
|
46
10
|
const docBrowser = useDocBrowser();
|
|
11
|
+
const { language, setLanguage } = useI18n();
|
|
12
|
+
const currentLanguageLabel = LANGUAGE_OPTIONS.find((option) => option.value === language)?.label ?? language;
|
|
13
|
+
|
|
14
|
+
const handleLanguageSwitch = (nextLanguage: I18nLanguage) => {
|
|
15
|
+
if (language === nextLanguage) {
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
setLanguage(nextLanguage);
|
|
19
|
+
window.location.reload();
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const navItems = [
|
|
23
|
+
{
|
|
24
|
+
target: '/model',
|
|
25
|
+
label: t('model'),
|
|
26
|
+
icon: Cpu,
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
target: '/providers',
|
|
30
|
+
label: t('providers'),
|
|
31
|
+
icon: Sparkles,
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
target: '/channels',
|
|
35
|
+
label: t('channels'),
|
|
36
|
+
icon: MessageSquare,
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
target: '/runtime',
|
|
40
|
+
label: t('runtime'),
|
|
41
|
+
icon: GitBranch,
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
target: '/sessions',
|
|
45
|
+
label: t('sessions'),
|
|
46
|
+
icon: History,
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
target: '/cron',
|
|
50
|
+
label: t('cron'),
|
|
51
|
+
icon: AlarmClock,
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
target: '/marketplace',
|
|
55
|
+
label: t('marketplace'),
|
|
56
|
+
icon: Store,
|
|
57
|
+
}
|
|
58
|
+
];
|
|
47
59
|
|
|
48
60
|
return (
|
|
49
61
|
<aside className="w-[240px] bg-[#f0f2f7] flex flex-col h-full py-6 px-4">
|
|
@@ -92,6 +104,24 @@ export function Sidebar() {
|
|
|
92
104
|
|
|
93
105
|
{/* Help Button */}
|
|
94
106
|
<div className="pt-3 border-t border-[#dde0ea] mt-3">
|
|
107
|
+
<div className="mb-2">
|
|
108
|
+
<Select value={language} onValueChange={(value) => handleLanguageSwitch(value as I18nLanguage)}>
|
|
109
|
+
<SelectTrigger className="w-full h-auto rounded-xl border-0 bg-transparent shadow-none px-3 py-2.5 text-[14px] font-medium text-gray-600 hover:bg-[#e4e7ef] focus:ring-0">
|
|
110
|
+
<div className="flex items-center gap-3 min-w-0">
|
|
111
|
+
<Languages className="h-[17px] w-[17px] text-gray-400" />
|
|
112
|
+
<span className="text-left">{t('language')}</span>
|
|
113
|
+
</div>
|
|
114
|
+
<span className="ml-auto text-xs text-gray-500">{currentLanguageLabel}</span>
|
|
115
|
+
</SelectTrigger>
|
|
116
|
+
<SelectContent>
|
|
117
|
+
{LANGUAGE_OPTIONS.map((option) => (
|
|
118
|
+
<SelectItem key={option.value} value={option.value} className="text-xs">
|
|
119
|
+
{option.label}
|
|
120
|
+
</SelectItem>
|
|
121
|
+
))}
|
|
122
|
+
</SelectContent>
|
|
123
|
+
</Select>
|
|
124
|
+
</div>
|
|
95
125
|
<button
|
|
96
126
|
onClick={() => docBrowser.open()}
|
|
97
127
|
className="w-full flex items-center gap-3 px-3 py-2.5 rounded-xl text-[14px] font-medium transition-all duration-base text-gray-600 hover:bg-[#e4e7ef] hover:text-gray-800"
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { cn } from '@/lib/utils';
|
|
3
|
+
|
|
4
|
+
/* ============================================================================
|
|
5
|
+
PageLayout — Unified page container
|
|
6
|
+
============================================================================ */
|
|
7
|
+
|
|
8
|
+
interface PageLayoutProps {
|
|
9
|
+
children: React.ReactNode;
|
|
10
|
+
/** When true, the page fills the full viewport height (e.g. Sessions, Cron) */
|
|
11
|
+
fullHeight?: boolean;
|
|
12
|
+
className?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function PageLayout({ children, fullHeight = false, className }: PageLayoutProps) {
|
|
16
|
+
return (
|
|
17
|
+
<div
|
|
18
|
+
className={cn(
|
|
19
|
+
'animate-fade-in',
|
|
20
|
+
fullHeight
|
|
21
|
+
? 'h-[calc(100vh-80px)] w-full flex flex-col'
|
|
22
|
+
: 'pb-16',
|
|
23
|
+
className
|
|
24
|
+
)}
|
|
25
|
+
>
|
|
26
|
+
{children}
|
|
27
|
+
</div>
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/* ============================================================================
|
|
32
|
+
PageHeader — Unified page title + subtitle + optional actions
|
|
33
|
+
============================================================================ */
|
|
34
|
+
|
|
35
|
+
interface PageHeaderProps {
|
|
36
|
+
title: string;
|
|
37
|
+
description?: string;
|
|
38
|
+
actions?: React.ReactNode;
|
|
39
|
+
className?: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function PageHeader({ title, description, actions, className }: PageHeaderProps) {
|
|
43
|
+
return (
|
|
44
|
+
<div className={cn('flex items-center justify-between mb-6 shrink-0', className)}>
|
|
45
|
+
<div>
|
|
46
|
+
<h2 className="text-xl font-semibold text-gray-900">{title}</h2>
|
|
47
|
+
{description && (
|
|
48
|
+
<p className="text-sm text-gray-500 mt-1">{description}</p>
|
|
49
|
+
)}
|
|
50
|
+
</div>
|
|
51
|
+
{actions && <div className="flex items-center gap-2 shrink-0">{actions}</div>}
|
|
52
|
+
</div>
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/* ============================================================================
|
|
57
|
+
PageBody — Unified body container (flex-1 when inside fullHeight layout)
|
|
58
|
+
============================================================================ */
|
|
59
|
+
|
|
60
|
+
interface PageBodyProps {
|
|
61
|
+
children: React.ReactNode;
|
|
62
|
+
className?: string;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function PageBody({ children, className }: PageBodyProps) {
|
|
66
|
+
return (
|
|
67
|
+
<div className={cn('flex-1 min-h-0', className)}>
|
|
68
|
+
{children}
|
|
69
|
+
</div>
|
|
70
|
+
);
|
|
71
|
+
}
|
|
@@ -10,6 +10,8 @@ import {
|
|
|
10
10
|
useMarketplaceInstalled,
|
|
11
11
|
useMarketplaceItems
|
|
12
12
|
} from '@/hooks/useMarketplace';
|
|
13
|
+
import { t } from '@/lib/i18n';
|
|
14
|
+
import { PageLayout, PageHeader } from '@/components/layout/page-layout';
|
|
13
15
|
import { cn } from '@/lib/utils';
|
|
14
16
|
import { PackageSearch } from 'lucide-react';
|
|
15
17
|
import { useEffect, useMemo, useState } from 'react';
|
|
@@ -180,16 +182,16 @@ function FilterPanel(props: {
|
|
|
180
182
|
<input
|
|
181
183
|
value={props.searchText}
|
|
182
184
|
onChange={(event) => props.onSearchTextChange(event.target.value)}
|
|
183
|
-
placeholder=
|
|
185
|
+
placeholder={t('marketplaceSearchPlaceholder')}
|
|
184
186
|
className="w-full h-9 border border-gray-200/80 rounded-xl pl-9 pr-3 text-sm focus:outline-none focus:ring-1 focus:ring-primary/40"
|
|
185
187
|
/>
|
|
186
188
|
</div>
|
|
187
189
|
|
|
188
190
|
<div className="inline-flex h-9 rounded-xl bg-gray-100/80 p-1 shrink-0">
|
|
189
191
|
{([
|
|
190
|
-
{ value: 'all', label: '
|
|
191
|
-
{ value: 'plugin', label: '
|
|
192
|
-
{ value: 'skill', label: '
|
|
192
|
+
{ value: 'all', label: t('marketplaceFilterAll') },
|
|
193
|
+
{ value: 'plugin', label: t('marketplaceFilterPlugins') },
|
|
194
|
+
{ value: 'skill', label: t('marketplaceFilterSkills') },
|
|
193
195
|
] as const).map((opt) => (
|
|
194
196
|
<button
|
|
195
197
|
key={opt.value}
|
|
@@ -213,8 +215,8 @@ function FilterPanel(props: {
|
|
|
213
215
|
<SelectValue />
|
|
214
216
|
</SelectTrigger>
|
|
215
217
|
<SelectContent>
|
|
216
|
-
<SelectItem value="relevance">
|
|
217
|
-
<SelectItem value="updated">
|
|
218
|
+
<SelectItem value="relevance">{t('marketplaceSortRelevance')}</SelectItem>
|
|
219
|
+
<SelectItem value="updated">{t('marketplaceSortUpdated')}</SelectItem>
|
|
218
220
|
</SelectContent>
|
|
219
221
|
</Select>
|
|
220
222
|
)}
|
|
@@ -234,8 +236,8 @@ function MarketplaceListCard(props: {
|
|
|
234
236
|
const record = props.record;
|
|
235
237
|
const pluginRecord = record?.type === 'plugin' ? record : undefined;
|
|
236
238
|
const type = props.item?.type ?? record?.type;
|
|
237
|
-
const title = props.item?.name ?? record?.label ?? record?.id ?? record?.spec ?? '
|
|
238
|
-
const summary = props.item?.summary ?? (record ? '
|
|
239
|
+
const title = props.item?.name ?? record?.label ?? record?.id ?? record?.spec ?? t('marketplaceUnknownItem');
|
|
240
|
+
const summary = props.item?.summary ?? (record ? t('marketplaceInstalledLocalSummary') : '');
|
|
239
241
|
const spec = props.item?.install.spec ?? record?.spec ?? '';
|
|
240
242
|
|
|
241
243
|
const targetId = record?.id || record?.spec;
|
|
@@ -249,12 +251,12 @@ function MarketplaceListCard(props: {
|
|
|
249
251
|
const isDisabled = record ? (record.enabled === false || record.runtimeStatus === 'disabled') : false;
|
|
250
252
|
const isInstalling = props.installState.isPending && props.item && props.installState.installingSpec === props.item.install.spec;
|
|
251
253
|
|
|
252
|
-
const displayType = type === 'plugin' ? '
|
|
254
|
+
const displayType = type === 'plugin' ? t('marketplaceTypePlugin') : type === 'skill' ? t('marketplaceTypeSkill') : t('marketplaceTypeExtension');
|
|
253
255
|
|
|
254
256
|
return (
|
|
255
257
|
<article className="group bg-white border border-gray-200/40 hover:border-gray-200/80 rounded-2xl px-5 py-4 hover:shadow-md shadow-sm transition-all flex items-start gap-3.5 justify-between cursor-default">
|
|
256
258
|
<div className="flex gap-3 min-w-0 flex-1 h-full items-start">
|
|
257
|
-
<ItemIcon name={title} fallback={spec || '
|
|
259
|
+
<ItemIcon name={title} fallback={spec || t('marketplaceTypeExtension')} />
|
|
258
260
|
<div className="min-w-0 flex-1 flex flex-col justify-center h-full">
|
|
259
261
|
<TooltipProvider delayDuration={400}>
|
|
260
262
|
<Tooltip>
|
|
@@ -304,7 +306,7 @@ function MarketplaceListCard(props: {
|
|
|
304
306
|
disabled={props.installState.isPending}
|
|
305
307
|
className="inline-flex items-center gap-1.5 h-8 px-4 rounded-xl text-xs font-medium bg-primary text-white hover:bg-primary-600 disabled:opacity-50 transition-colors"
|
|
306
308
|
>
|
|
307
|
-
{isInstalling ? '
|
|
309
|
+
{isInstalling ? t('marketplaceInstalling') : t('marketplaceInstall')}
|
|
308
310
|
</button>
|
|
309
311
|
)}
|
|
310
312
|
|
|
@@ -315,8 +317,8 @@ function MarketplaceListCard(props: {
|
|
|
315
317
|
className="inline-flex items-center h-8 px-4 rounded-xl text-xs font-medium border border-gray-200/80 text-gray-600 bg-white hover:bg-gray-50 hover:border-gray-300 disabled:opacity-50 transition-colors"
|
|
316
318
|
>
|
|
317
319
|
{busyForRecord && props.manageState.action !== 'uninstall'
|
|
318
|
-
? (props.manageState.action === 'enable' ? '
|
|
319
|
-
: (isDisabled ? '
|
|
320
|
+
? (props.manageState.action === 'enable' ? t('marketplaceEnabling') : t('marketplaceDisabling'))
|
|
321
|
+
: (isDisabled ? t('marketplaceEnable') : t('marketplaceDisable'))}
|
|
320
322
|
</button>
|
|
321
323
|
)}
|
|
322
324
|
|
|
@@ -326,7 +328,7 @@ function MarketplaceListCard(props: {
|
|
|
326
328
|
onClick={() => props.onManage('uninstall', record)}
|
|
327
329
|
className="inline-flex items-center h-8 px-4 rounded-xl text-xs font-medium border border-rose-100 text-rose-500 bg-white hover:bg-rose-50 hover:border-rose-200 disabled:opacity-50 transition-colors"
|
|
328
330
|
>
|
|
329
|
-
{busyForRecord && props.manageState.action === 'uninstall' ? '
|
|
331
|
+
{busyForRecord && props.manageState.action === 'uninstall' ? t('marketplaceRemoving') : t('marketplaceUninstall')}
|
|
330
332
|
</button>
|
|
331
333
|
)}
|
|
332
334
|
</div>
|
|
@@ -348,7 +350,7 @@ function PaginationBar(props: {
|
|
|
348
350
|
onClick={props.onPrev}
|
|
349
351
|
disabled={props.page <= 1 || props.busy}
|
|
350
352
|
>
|
|
351
|
-
|
|
353
|
+
{t('prev')}
|
|
352
354
|
</button>
|
|
353
355
|
<div className="text-sm text-gray-600 min-w-20 text-center">
|
|
354
356
|
{props.totalPages === 0 ? '0 / 0' : `${props.page} / ${props.totalPages}`}
|
|
@@ -358,7 +360,7 @@ function PaginationBar(props: {
|
|
|
358
360
|
onClick={props.onNext}
|
|
359
361
|
disabled={props.totalPages === 0 || props.page >= props.totalPages || props.busy}
|
|
360
362
|
>
|
|
361
|
-
|
|
363
|
+
{t('next')}
|
|
362
364
|
</button>
|
|
363
365
|
</div>
|
|
364
366
|
);
|
|
@@ -446,13 +448,13 @@ export function MarketplacePage() {
|
|
|
446
448
|
const listSummary = useMemo(() => {
|
|
447
449
|
if (scope === 'installed') {
|
|
448
450
|
if (installedQuery.isLoading) {
|
|
449
|
-
return '
|
|
451
|
+
return t('loading');
|
|
450
452
|
}
|
|
451
|
-
return `${installedEntries.length}
|
|
453
|
+
return `${installedEntries.length} ${t('marketplaceInstalledCountSuffix')}`;
|
|
452
454
|
}
|
|
453
455
|
|
|
454
456
|
if (!itemsQuery.data) {
|
|
455
|
-
return '
|
|
457
|
+
return t('loading');
|
|
456
458
|
}
|
|
457
459
|
|
|
458
460
|
return `${allItems.length} / ${total}`;
|
|
@@ -470,8 +472,8 @@ export function MarketplacePage() {
|
|
|
470
472
|
};
|
|
471
473
|
|
|
472
474
|
const tabs = [
|
|
473
|
-
{ id: 'all', label: '
|
|
474
|
-
{ id: 'installed', label: '
|
|
475
|
+
{ id: 'all', label: t('marketplaceTabMarketplace') },
|
|
476
|
+
{ id: 'installed', label: t('marketplaceTabInstalled'), count: installedQuery.data?.total ?? 0 }
|
|
475
477
|
];
|
|
476
478
|
|
|
477
479
|
const handleInstall = (item: MarketplaceItemSummary) => {
|
|
@@ -493,9 +495,9 @@ export function MarketplacePage() {
|
|
|
493
495
|
|
|
494
496
|
if (action === 'uninstall') {
|
|
495
497
|
const confirmed = await confirm({
|
|
496
|
-
title:
|
|
497
|
-
description: '
|
|
498
|
-
confirmLabel: '
|
|
498
|
+
title: `${t('marketplaceUninstallTitle')} ${targetId}?`,
|
|
499
|
+
description: t('marketplaceUninstallDescription'),
|
|
500
|
+
confirmLabel: t('marketplaceUninstall'),
|
|
499
501
|
variant: 'destructive'
|
|
500
502
|
});
|
|
501
503
|
if (!confirmed) {
|
|
@@ -512,11 +514,8 @@ export function MarketplacePage() {
|
|
|
512
514
|
};
|
|
513
515
|
|
|
514
516
|
return (
|
|
515
|
-
<
|
|
516
|
-
<
|
|
517
|
-
<h2 className="text-xl font-semibold text-gray-900">Marketplace</h2>
|
|
518
|
-
<p className="text-[12px] text-gray-400 mt-0.5">A cleaner extension list focused on install / enable / disable.</p>
|
|
519
|
-
</div>
|
|
517
|
+
<PageLayout>
|
|
518
|
+
<PageHeader title={t('marketplacePageTitle')} description={t('marketplacePageDescription')} />
|
|
520
519
|
|
|
521
520
|
<Tabs
|
|
522
521
|
tabs={tabs}
|
|
@@ -546,18 +545,18 @@ export function MarketplacePage() {
|
|
|
546
545
|
|
|
547
546
|
<section>
|
|
548
547
|
<div className="flex items-center justify-between mb-3">
|
|
549
|
-
<h3 className="text-[14px] font-semibold text-gray-900">{scope === 'installed' ? '
|
|
548
|
+
<h3 className="text-[14px] font-semibold text-gray-900">{scope === 'installed' ? t('marketplaceSectionInstalled') : t('marketplaceSectionExtensions')}</h3>
|
|
550
549
|
<span className="text-[12px] text-gray-500">{listSummary}</span>
|
|
551
550
|
</div>
|
|
552
551
|
|
|
553
552
|
{scope === 'all' && itemsQuery.isError && (
|
|
554
553
|
<div className="p-4 rounded-xl bg-rose-50 border border-rose-200 text-rose-700 text-sm">
|
|
555
|
-
|
|
554
|
+
{t('marketplaceErrorLoadingData')}: {itemsQuery.error.message}
|
|
556
555
|
</div>
|
|
557
556
|
)}
|
|
558
557
|
{scope === 'installed' && installedQuery.isError && (
|
|
559
558
|
<div className="p-4 rounded-xl bg-rose-50 border border-rose-200 text-rose-700 text-sm">
|
|
560
|
-
|
|
559
|
+
{t('marketplaceErrorLoadingInstalled')}: {installedQuery.error.message}
|
|
561
560
|
</div>
|
|
562
561
|
)}
|
|
563
562
|
|
|
@@ -588,10 +587,10 @@ export function MarketplacePage() {
|
|
|
588
587
|
</div>
|
|
589
588
|
|
|
590
589
|
{scope === 'all' && !itemsQuery.isLoading && !itemsQuery.isError && allItems.length === 0 && (
|
|
591
|
-
<div className="text-[13px] text-gray-500 py-8 text-center">
|
|
590
|
+
<div className="text-[13px] text-gray-500 py-8 text-center">{t('marketplaceNoItems')}</div>
|
|
592
591
|
)}
|
|
593
592
|
{scope === 'installed' && !installedQuery.isLoading && !installedQuery.isError && installedEntries.length === 0 && (
|
|
594
|
-
<div className="text-[13px] text-gray-500 py-8 text-center">
|
|
593
|
+
<div className="text-[13px] text-gray-500 py-8 text-center">{t('marketplaceNoInstalledItems')}</div>
|
|
595
594
|
)}
|
|
596
595
|
</section>
|
|
597
596
|
|
|
@@ -605,6 +604,6 @@ export function MarketplacePage() {
|
|
|
605
604
|
/>
|
|
606
605
|
)}
|
|
607
606
|
<ConfirmDialog />
|
|
608
|
-
</
|
|
607
|
+
</PageLayout>
|
|
609
608
|
);
|
|
610
609
|
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createContext,
|
|
3
|
+
useCallback,
|
|
4
|
+
useContext,
|
|
5
|
+
useEffect,
|
|
6
|
+
useMemo,
|
|
7
|
+
useState,
|
|
8
|
+
type ReactNode,
|
|
9
|
+
} from 'react';
|
|
10
|
+
import {
|
|
11
|
+
getLanguage,
|
|
12
|
+
initializeI18n,
|
|
13
|
+
setLanguage as applyLanguage,
|
|
14
|
+
subscribeLanguageChange,
|
|
15
|
+
t,
|
|
16
|
+
type I18nLanguage,
|
|
17
|
+
} from '@/lib/i18n';
|
|
18
|
+
|
|
19
|
+
type I18nContextValue = {
|
|
20
|
+
language: I18nLanguage;
|
|
21
|
+
setLanguage: (lang: I18nLanguage) => void;
|
|
22
|
+
toggleLanguage: () => void;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const I18nContext = createContext<I18nContextValue | null>(null);
|
|
26
|
+
|
|
27
|
+
export function I18nProvider({ children }: { children: ReactNode }) {
|
|
28
|
+
const [language, setLanguageState] = useState<I18nLanguage>(() => initializeI18n());
|
|
29
|
+
|
|
30
|
+
useEffect(() => {
|
|
31
|
+
const unsubscribe = subscribeLanguageChange((next) => {
|
|
32
|
+
setLanguageState(next);
|
|
33
|
+
});
|
|
34
|
+
return unsubscribe;
|
|
35
|
+
}, []);
|
|
36
|
+
|
|
37
|
+
const setLanguage = useCallback((lang: I18nLanguage) => {
|
|
38
|
+
applyLanguage(lang);
|
|
39
|
+
setLanguageState(getLanguage());
|
|
40
|
+
}, []);
|
|
41
|
+
|
|
42
|
+
const toggleLanguage = useCallback(() => {
|
|
43
|
+
setLanguage(language === 'en' ? 'zh' : 'en');
|
|
44
|
+
}, [language, setLanguage]);
|
|
45
|
+
|
|
46
|
+
// Ensure descendants re-render when language changes; most text calls global t().
|
|
47
|
+
const value = useMemo(
|
|
48
|
+
() => ({ language, setLanguage, toggleLanguage }),
|
|
49
|
+
[language, setLanguage, toggleLanguage]
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
return <I18nContext.Provider value={value}>{children}</I18nContext.Provider>;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function useI18n(): I18nContextValue & { t: typeof t } {
|
|
56
|
+
const ctx = useContext(I18nContext);
|
|
57
|
+
if (!ctx) {
|
|
58
|
+
throw new Error('useI18n must be used within I18nProvider');
|
|
59
|
+
}
|
|
60
|
+
return {
|
|
61
|
+
...ctx,
|
|
62
|
+
t,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
DialogTitle
|
|
9
9
|
} from '@/components/ui/dialog';
|
|
10
10
|
import { Button } from '@/components/ui/button';
|
|
11
|
+
import { t } from '@/lib/i18n';
|
|
11
12
|
|
|
12
13
|
export type ConfirmDialogVariant = 'default' | 'destructive';
|
|
13
14
|
|
|
@@ -28,8 +29,8 @@ export const ConfirmDialog = ({
|
|
|
28
29
|
onOpenChange,
|
|
29
30
|
title,
|
|
30
31
|
description,
|
|
31
|
-
confirmLabel = '
|
|
32
|
-
cancelLabel = '
|
|
32
|
+
confirmLabel = t('confirm'),
|
|
33
|
+
cancelLabel = t('cancel'),
|
|
33
34
|
variant = 'default',
|
|
34
35
|
onConfirm,
|
|
35
36
|
onCancel
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { cn } from '@/lib/utils';
|
|
3
|
+
import { formatNumber } from '@/lib/i18n';
|
|
3
4
|
|
|
4
5
|
interface Tab {
|
|
5
6
|
id: string;
|
|
@@ -35,7 +36,7 @@ export function Tabs({ tabs, activeTab, onChange, className }: TabsProps) {
|
|
|
35
36
|
<span className={cn(
|
|
36
37
|
'text-[11px] font-medium',
|
|
37
38
|
isActive ? 'text-gray-500' : 'text-gray-500'
|
|
38
|
-
)}>{tab.count
|
|
39
|
+
)}>{formatNumber(tab.count)}</span>
|
|
39
40
|
)}
|
|
40
41
|
{isActive && (
|
|
41
42
|
<div className="absolute bottom-0 left-0 right-0 h-[2px] bg-primary rounded-full" />
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { ReactNode } from 'react';
|
|
2
2
|
import { useCallback, useState } from 'react';
|
|
3
3
|
import { ConfirmDialog } from '@/components/ui/confirm-dialog';
|
|
4
|
+
import { t } from '@/lib/i18n';
|
|
4
5
|
|
|
5
6
|
export type ConfirmOptions = {
|
|
6
7
|
title: string;
|
|
@@ -24,8 +25,8 @@ const initial: ConfirmState = {
|
|
|
24
25
|
open: false,
|
|
25
26
|
title: '',
|
|
26
27
|
description: '',
|
|
27
|
-
confirmLabel: '
|
|
28
|
-
cancelLabel: '
|
|
28
|
+
confirmLabel: t('confirm'),
|
|
29
|
+
cancelLabel: t('cancel'),
|
|
29
30
|
variant: 'default',
|
|
30
31
|
resolve: null
|
|
31
32
|
};
|
|
@@ -42,8 +43,8 @@ export function useConfirmDialog(): {
|
|
|
42
43
|
open: true,
|
|
43
44
|
title: options.title,
|
|
44
45
|
description: options.description ?? '',
|
|
45
|
-
confirmLabel: options.confirmLabel ?? '
|
|
46
|
-
cancelLabel: options.cancelLabel ?? '
|
|
46
|
+
confirmLabel: options.confirmLabel ?? t('confirm'),
|
|
47
|
+
cancelLabel: options.cancelLabel ?? t('cancel'),
|
|
47
48
|
variant: options.variant ?? 'default',
|
|
48
49
|
resolve: (value) => {
|
|
49
50
|
resolve(value);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
|
2
2
|
import { toast } from 'sonner';
|
|
3
|
+
import { t } from '@/lib/i18n';
|
|
3
4
|
import {
|
|
4
5
|
fetchMarketplaceItem,
|
|
5
6
|
fetchMarketplaceInstalled,
|
|
@@ -53,10 +54,10 @@ export function useInstallMarketplaceItem() {
|
|
|
53
54
|
queryClient.invalidateQueries({ queryKey: ['marketplace-installed'] });
|
|
54
55
|
queryClient.refetchQueries({ queryKey: ['marketplace-installed'], type: 'active' });
|
|
55
56
|
queryClient.refetchQueries({ queryKey: ['marketplace-items'], type: 'active' });
|
|
56
|
-
toast.success(result.message || `${result.type}
|
|
57
|
+
toast.success(result.message || `${result.type} ${t('marketplaceInstalledCountSuffix')}`);
|
|
57
58
|
},
|
|
58
59
|
onError: (error: Error) => {
|
|
59
|
-
toast.error(error.message || '
|
|
60
|
+
toast.error(error.message || t('marketplaceInstallFailed'));
|
|
60
61
|
}
|
|
61
62
|
});
|
|
62
63
|
}
|
|
@@ -74,7 +75,7 @@ export function useManageMarketplaceItem() {
|
|
|
74
75
|
toast.success(result.message || `${result.action} success`);
|
|
75
76
|
},
|
|
76
77
|
onError: (error: Error) => {
|
|
77
|
-
toast.error(error.message || '
|
|
78
|
+
toast.error(error.message || t('marketplaceOperationFailed'));
|
|
78
79
|
}
|
|
79
80
|
});
|
|
80
81
|
}
|