@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.
@@ -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
- const navItems = [
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="Search extensions..."
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: 'All' },
191
- { value: 'plugin', label: 'Plugins' },
192
- { value: 'skill', label: 'Skills' },
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">Relevance</SelectItem>
217
- <SelectItem value="updated">Recently Updated</SelectItem>
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 ?? 'Unknown Item';
238
- const summary = props.item?.summary ?? (record ? 'Installed locally. Details are currently unavailable from marketplace.' : '');
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' ? 'Plugin' : type === 'skill' ? 'Skill' : 'Extension';
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 || 'Ext'} />
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 ? 'Installing...' : 'Install'}
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' ? 'Enabling...' : 'Disabling...')
319
- : (isDisabled ? 'Enable' : 'Disable')}
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' ? 'Removing...' : '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
- Prev
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
- Next
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 'Loading...';
451
+ return t('loading');
450
452
  }
451
- return `${installedEntries.length} installed`;
453
+ return `${installedEntries.length} ${t('marketplaceInstalledCountSuffix')}`;
452
454
  }
453
455
 
454
456
  if (!itemsQuery.data) {
455
- return 'Loading...';
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: 'Marketplace' },
474
- { id: 'installed', label: 'Installed', count: installedQuery.data?.total ?? 0 }
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: `Uninstall ${targetId}?`,
497
- description: 'This will remove the extension. You can install it again from the marketplace.',
498
- confirmLabel: 'Uninstall',
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
- <div className="animate-fade-in pb-20">
516
- <div className="mb-4">
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' ? 'Installed' : 'Extensions'}</h3>
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
- Failed to load marketplace data: {itemsQuery.error.message}
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
- Failed to load installed items: {installedQuery.error.message}
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">No items found.</div>
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">No installed items found.</div>
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
- </div>
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 = 'Confirm',
32
- cancelLabel = 'Cancel',
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.toLocaleString()}</span>
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: 'Confirm',
28
- cancelLabel: 'Cancel',
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 ?? 'Confirm',
46
- cancelLabel: options.cancelLabel ?? 'Cancel',
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} installed`);
57
+ toast.success(result.message || `${result.type} ${t('marketplaceInstalledCountSuffix')}`);
57
58
  },
58
59
  onError: (error: Error) => {
59
- toast.error(error.message || 'Install failed');
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 || 'Operation failed');
78
+ toast.error(error.message || t('marketplaceOperationFailed'));
78
79
  }
79
80
  });
80
81
  }