@nextclaw/ui 0.5.8 → 0.5.10

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.
@@ -10,6 +10,7 @@ import {
10
10
  useMarketplaceInstalled,
11
11
  useMarketplaceItems
12
12
  } from '@/hooks/useMarketplace';
13
+ import { t } from '@/lib/i18n';
13
14
  import { cn } from '@/lib/utils';
14
15
  import { PackageSearch } from 'lucide-react';
15
16
  import { useEffect, useMemo, useState } from 'react';
@@ -180,16 +181,16 @@ function FilterPanel(props: {
180
181
  <input
181
182
  value={props.searchText}
182
183
  onChange={(event) => props.onSearchTextChange(event.target.value)}
183
- placeholder="Search extensions..."
184
+ placeholder={t('marketplaceSearchPlaceholder')}
184
185
  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
186
  />
186
187
  </div>
187
188
 
188
189
  <div className="inline-flex h-9 rounded-xl bg-gray-100/80 p-1 shrink-0">
189
190
  {([
190
- { value: 'all', label: 'All' },
191
- { value: 'plugin', label: 'Plugins' },
192
- { value: 'skill', label: 'Skills' },
191
+ { value: 'all', label: t('marketplaceFilterAll') },
192
+ { value: 'plugin', label: t('marketplaceFilterPlugins') },
193
+ { value: 'skill', label: t('marketplaceFilterSkills') },
193
194
  ] as const).map((opt) => (
194
195
  <button
195
196
  key={opt.value}
@@ -213,8 +214,8 @@ function FilterPanel(props: {
213
214
  <SelectValue />
214
215
  </SelectTrigger>
215
216
  <SelectContent>
216
- <SelectItem value="relevance">Relevance</SelectItem>
217
- <SelectItem value="updated">Recently Updated</SelectItem>
217
+ <SelectItem value="relevance">{t('marketplaceSortRelevance')}</SelectItem>
218
+ <SelectItem value="updated">{t('marketplaceSortUpdated')}</SelectItem>
218
219
  </SelectContent>
219
220
  </Select>
220
221
  )}
@@ -234,8 +235,8 @@ function MarketplaceListCard(props: {
234
235
  const record = props.record;
235
236
  const pluginRecord = record?.type === 'plugin' ? record : undefined;
236
237
  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.' : '');
238
+ const title = props.item?.name ?? record?.label ?? record?.id ?? record?.spec ?? t('marketplaceUnknownItem');
239
+ const summary = props.item?.summary ?? (record ? t('marketplaceInstalledLocalSummary') : '');
239
240
  const spec = props.item?.install.spec ?? record?.spec ?? '';
240
241
 
241
242
  const targetId = record?.id || record?.spec;
@@ -249,12 +250,12 @@ function MarketplaceListCard(props: {
249
250
  const isDisabled = record ? (record.enabled === false || record.runtimeStatus === 'disabled') : false;
250
251
  const isInstalling = props.installState.isPending && props.item && props.installState.installingSpec === props.item.install.spec;
251
252
 
252
- const displayType = type === 'plugin' ? 'Plugin' : type === 'skill' ? 'Skill' : 'Extension';
253
+ const displayType = type === 'plugin' ? t('marketplaceTypePlugin') : type === 'skill' ? t('marketplaceTypeSkill') : t('marketplaceTypeExtension');
253
254
 
254
255
  return (
255
256
  <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
257
  <div className="flex gap-3 min-w-0 flex-1 h-full items-start">
257
- <ItemIcon name={title} fallback={spec || 'Ext'} />
258
+ <ItemIcon name={title} fallback={spec || t('marketplaceTypeExtension')} />
258
259
  <div className="min-w-0 flex-1 flex flex-col justify-center h-full">
259
260
  <TooltipProvider delayDuration={400}>
260
261
  <Tooltip>
@@ -304,7 +305,7 @@ function MarketplaceListCard(props: {
304
305
  disabled={props.installState.isPending}
305
306
  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
307
  >
307
- {isInstalling ? 'Installing...' : 'Install'}
308
+ {isInstalling ? t('marketplaceInstalling') : t('marketplaceInstall')}
308
309
  </button>
309
310
  )}
310
311
 
@@ -315,8 +316,8 @@ function MarketplaceListCard(props: {
315
316
  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
317
  >
317
318
  {busyForRecord && props.manageState.action !== 'uninstall'
318
- ? (props.manageState.action === 'enable' ? 'Enabling...' : 'Disabling...')
319
- : (isDisabled ? 'Enable' : 'Disable')}
319
+ ? (props.manageState.action === 'enable' ? t('marketplaceEnabling') : t('marketplaceDisabling'))
320
+ : (isDisabled ? t('marketplaceEnable') : t('marketplaceDisable'))}
320
321
  </button>
321
322
  )}
322
323
 
@@ -326,7 +327,7 @@ function MarketplaceListCard(props: {
326
327
  onClick={() => props.onManage('uninstall', record)}
327
328
  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
329
  >
329
- {busyForRecord && props.manageState.action === 'uninstall' ? 'Removing...' : 'Uninstall'}
330
+ {busyForRecord && props.manageState.action === 'uninstall' ? t('marketplaceRemoving') : t('marketplaceUninstall')}
330
331
  </button>
331
332
  )}
332
333
  </div>
@@ -348,7 +349,7 @@ function PaginationBar(props: {
348
349
  onClick={props.onPrev}
349
350
  disabled={props.page <= 1 || props.busy}
350
351
  >
351
- Prev
352
+ {t('prev')}
352
353
  </button>
353
354
  <div className="text-sm text-gray-600 min-w-20 text-center">
354
355
  {props.totalPages === 0 ? '0 / 0' : `${props.page} / ${props.totalPages}`}
@@ -358,7 +359,7 @@ function PaginationBar(props: {
358
359
  onClick={props.onNext}
359
360
  disabled={props.totalPages === 0 || props.page >= props.totalPages || props.busy}
360
361
  >
361
- Next
362
+ {t('next')}
362
363
  </button>
363
364
  </div>
364
365
  );
@@ -446,13 +447,13 @@ export function MarketplacePage() {
446
447
  const listSummary = useMemo(() => {
447
448
  if (scope === 'installed') {
448
449
  if (installedQuery.isLoading) {
449
- return 'Loading...';
450
+ return t('loading');
450
451
  }
451
- return `${installedEntries.length} installed`;
452
+ return `${installedEntries.length} ${t('marketplaceInstalledCountSuffix')}`;
452
453
  }
453
454
 
454
455
  if (!itemsQuery.data) {
455
- return 'Loading...';
456
+ return t('loading');
456
457
  }
457
458
 
458
459
  return `${allItems.length} / ${total}`;
@@ -470,8 +471,8 @@ export function MarketplacePage() {
470
471
  };
471
472
 
472
473
  const tabs = [
473
- { id: 'all', label: 'Marketplace' },
474
- { id: 'installed', label: 'Installed', count: installedQuery.data?.total ?? 0 }
474
+ { id: 'all', label: t('marketplaceTabMarketplace') },
475
+ { id: 'installed', label: t('marketplaceTabInstalled'), count: installedQuery.data?.total ?? 0 }
475
476
  ];
476
477
 
477
478
  const handleInstall = (item: MarketplaceItemSummary) => {
@@ -493,9 +494,9 @@ export function MarketplacePage() {
493
494
 
494
495
  if (action === 'uninstall') {
495
496
  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',
497
+ title: `${t('marketplaceUninstallTitle')} ${targetId}?`,
498
+ description: t('marketplaceUninstallDescription'),
499
+ confirmLabel: t('marketplaceUninstall'),
499
500
  variant: 'destructive'
500
501
  });
501
502
  if (!confirmed) {
@@ -514,8 +515,8 @@ export function MarketplacePage() {
514
515
  return (
515
516
  <div className="animate-fade-in pb-20">
516
517
  <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>
518
+ <h2 className="text-xl font-semibold text-gray-900">{t('marketplacePageTitle')}</h2>
519
+ <p className="text-[12px] text-gray-400 mt-0.5">{t('marketplacePageDescription')}</p>
519
520
  </div>
520
521
 
521
522
  <Tabs
@@ -546,18 +547,18 @@ export function MarketplacePage() {
546
547
 
547
548
  <section>
548
549
  <div className="flex items-center justify-between mb-3">
549
- <h3 className="text-[14px] font-semibold text-gray-900">{scope === 'installed' ? 'Installed' : 'Extensions'}</h3>
550
+ <h3 className="text-[14px] font-semibold text-gray-900">{scope === 'installed' ? t('marketplaceSectionInstalled') : t('marketplaceSectionExtensions')}</h3>
550
551
  <span className="text-[12px] text-gray-500">{listSummary}</span>
551
552
  </div>
552
553
 
553
554
  {scope === 'all' && itemsQuery.isError && (
554
555
  <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}
556
+ {t('marketplaceErrorLoadingData')}: {itemsQuery.error.message}
556
557
  </div>
557
558
  )}
558
559
  {scope === 'installed' && installedQuery.isError && (
559
560
  <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}
561
+ {t('marketplaceErrorLoadingInstalled')}: {installedQuery.error.message}
561
562
  </div>
562
563
  )}
563
564
 
@@ -588,10 +589,10 @@ export function MarketplacePage() {
588
589
  </div>
589
590
 
590
591
  {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>
592
+ <div className="text-[13px] text-gray-500 py-8 text-center">{t('marketplaceNoItems')}</div>
592
593
  )}
593
594
  {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>
595
+ <div className="text-[13px] text-gray-500 py-8 text-center">{t('marketplaceNoInstalledItems')}</div>
595
596
  )}
596
597
  </section>
597
598
 
@@ -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
  }