mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-06-02 03:55:55 +00:00
feat: add tag filtering functionality to Plugin Market
- Introduced TagsFilter component for selecting and filtering plugins by tags. - Updated PluginMarketComponent to handle tag selection and display. - Enhanced PluginMarketCardComponent to show selected tags. - Modified CloudServiceClient to fetch available tags from the API. - Updated localization files to support new tag-related strings.
This commit is contained in:
3611
web/pnpm-lock.yaml
generated
3611
web/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -3,7 +3,13 @@
|
||||
import styles from './layout.module.css';
|
||||
import HomeSidebar from '@/app/home/components/home-sidebar/HomeSidebar';
|
||||
import HomeTitleBar from '@/app/home/components/home-titlebar/HomeTitleBar';
|
||||
import React, { useState, useCallback, useMemo, useEffect } from 'react';
|
||||
import React, {
|
||||
useState,
|
||||
useCallback,
|
||||
useMemo,
|
||||
useEffect,
|
||||
Suspense,
|
||||
} from 'react';
|
||||
import { SidebarChildVO } from '@/app/home/components/home-sidebar/HomeSidebarChild';
|
||||
import { I18nObject } from '@/app/infra/entities/common';
|
||||
import { userInfo, initializeUserInfo } from '@/app/infra/http';
|
||||
@@ -39,7 +45,9 @@ export default function HomeLayout({
|
||||
return (
|
||||
<div className={styles.homeLayoutContainer}>
|
||||
<aside className={styles.sidebar}>
|
||||
<Suspense fallback={<div />}>
|
||||
<HomeSidebar onSelectedChangeAction={onSelectedChangeAction} />
|
||||
</Suspense>
|
||||
</aside>
|
||||
|
||||
<div className={styles.main}>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect, useCallback, useRef, Suspense } from 'react';
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import {
|
||||
Select,
|
||||
@@ -11,14 +10,7 @@ import {
|
||||
SelectValue,
|
||||
} from '@/components/ui/select';
|
||||
import { ToggleGroup, ToggleGroupItem } from '@/components/ui/toggle-group';
|
||||
import {
|
||||
Search,
|
||||
Loader2,
|
||||
Wrench,
|
||||
AudioWaveform,
|
||||
Hash,
|
||||
Book,
|
||||
} from 'lucide-react';
|
||||
import { Search, Wrench, AudioWaveform, Hash, Book } from 'lucide-react';
|
||||
import PluginMarketCardComponent from './plugin-market-card/PluginMarketCardComponent';
|
||||
import { PluginMarketCardVO } from './plugin-market-card/PluginMarketCardVO';
|
||||
import { getCloudServiceClientSync } from '@/app/infra/http';
|
||||
@@ -28,6 +20,8 @@ import { extractI18nObject } from '@/i18n/I18nProvider';
|
||||
import { toast } from 'sonner';
|
||||
import { ApiRespMarketplacePlugins } from '@/app/infra/entities/api';
|
||||
import { LoadingSpinner } from '@/components/ui/loading-spinner';
|
||||
import { TagsFilter } from './TagsFilter';
|
||||
import { PluginTag } from '@/app/infra/http/CloudServiceClient';
|
||||
|
||||
interface SortOption {
|
||||
value: string;
|
||||
@@ -43,10 +37,12 @@ function MarketPageContent({
|
||||
installPlugin: (plugin: PluginV4) => void;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const searchParams = useSearchParams();
|
||||
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [componentFilter, setComponentFilter] = useState<string>('all');
|
||||
const [selectedTags, setSelectedTags] = useState<string[]>([]);
|
||||
const [availableTags, setAvailableTags] = useState<PluginTag[]>([]);
|
||||
const [tagNames, setTagNames] = useState<Record<string, string>>({});
|
||||
const [plugins, setPlugins] = useState<PluginMarketCardVO[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isLoadingMore, setIsLoadingMore] = useState(false);
|
||||
@@ -112,6 +108,7 @@ function MarketPageContent({
|
||||
githubURL: plugin.repository,
|
||||
version: plugin.latest_version,
|
||||
components: plugin.components,
|
||||
tags: plugin.tags || [],
|
||||
});
|
||||
}, []);
|
||||
|
||||
@@ -129,7 +126,7 @@ function MarketPageContent({
|
||||
const filterValue =
|
||||
componentFilter === 'all' ? undefined : componentFilter;
|
||||
|
||||
// Always use searchMarketplacePlugins to support component filtering
|
||||
// Always use searchMarketplacePlugins to support component filtering and tags filtering
|
||||
const response =
|
||||
await getCloudServiceClientSync().searchMarketplacePlugins(
|
||||
isSearch && searchQuery.trim() ? searchQuery.trim() : '',
|
||||
@@ -138,6 +135,7 @@ function MarketPageContent({
|
||||
sortBy,
|
||||
sortOrder,
|
||||
filterValue,
|
||||
selectedTags.length > 0 ? selectedTags : undefined,
|
||||
);
|
||||
|
||||
const data: ApiRespMarketplacePlugins = response;
|
||||
@@ -166,6 +164,7 @@ function MarketPageContent({
|
||||
[
|
||||
searchQuery,
|
||||
componentFilter,
|
||||
selectedTags,
|
||||
pageSize,
|
||||
transformToVO,
|
||||
plugins.length,
|
||||
@@ -176,8 +175,34 @@ function MarketPageContent({
|
||||
// 初始加载
|
||||
useEffect(() => {
|
||||
fetchPlugins(1, false, true);
|
||||
fetchAvailableTags();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
// 获取可用标签
|
||||
const fetchAvailableTags = async () => {
|
||||
try {
|
||||
const response = await getCloudServiceClientSync().getAllTags();
|
||||
const tags = response.tags || [];
|
||||
setAvailableTags(tags);
|
||||
|
||||
// Build tag names map for all components to use
|
||||
const nameMap: Record<string, string> = {};
|
||||
tags.forEach((tag: PluginTag) => {
|
||||
const displayName = {
|
||||
en_US: tag.display_name.en_US || tag.tag,
|
||||
zh_Hans: tag.display_name.zh_Hans || tag.tag,
|
||||
zh_Hant: tag.display_name.zh_Hant,
|
||||
ja_JP: tag.display_name.ja_JP,
|
||||
};
|
||||
nameMap[tag.tag] = extractI18nObject(displayName);
|
||||
});
|
||||
setTagNames(nameMap);
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch tags:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// 搜索功能
|
||||
const handleSearch = useCallback(
|
||||
(query: string) => {
|
||||
@@ -228,16 +253,19 @@ function MarketPageContent({
|
||||
fetchPlugins(1, !!searchQuery.trim(), true);
|
||||
}, [sortOption, componentFilter]);
|
||||
|
||||
// 处理URL参数,重定向到 LangBot Space
|
||||
// Tags 筛选变化时重新搜索
|
||||
useEffect(() => {
|
||||
const author = searchParams.get('author');
|
||||
const pluginName = searchParams.get('plugin');
|
||||
|
||||
if (author && pluginName) {
|
||||
const detailUrl = `https://space.langbot.app/market/${author}/${pluginName}`;
|
||||
window.open(detailUrl, '_blank');
|
||||
if (!isLoading) {
|
||||
setCurrentPage(1);
|
||||
fetchPlugins(1, searchQuery.trim() !== '', true);
|
||||
}
|
||||
}, [searchParams]);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [selectedTags]);
|
||||
|
||||
// 处理 tags 变化
|
||||
const handleTagsChange = useCallback((tags: string[]) => {
|
||||
setSelectedTags(tags);
|
||||
}, []);
|
||||
|
||||
// 处理安装插件
|
||||
const handleInstallPlugin = useCallback(
|
||||
@@ -343,8 +371,8 @@ function MarketPageContent({
|
||||
<div className="h-full flex flex-col">
|
||||
{/* Fixed header with search and sort controls */}
|
||||
<div className="flex-shrink-0 space-y-4 px-3 sm:px-4 py-4 sm:py-6">
|
||||
{/* Search box */}
|
||||
<div className="flex items-center justify-center">
|
||||
{/* Search box and Tags filter */}
|
||||
<div className="flex flex-col sm:flex-row items-center justify-center gap-3">
|
||||
<div className="relative w-full max-w-2xl">
|
||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-muted-foreground h-4 w-4" />
|
||||
<Input
|
||||
@@ -363,6 +391,13 @@ function MarketPageContent({
|
||||
className="pl-10 pr-4 text-sm sm:text-base"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Tags filter */}
|
||||
<TagsFilter
|
||||
availableTags={availableTags}
|
||||
selectedTags={selectedTags}
|
||||
onTagsChange={handleTagsChange}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Component filter and sort */}
|
||||
@@ -477,6 +512,7 @@ function MarketPageContent({
|
||||
key={plugin.pluginId}
|
||||
cardVO={plugin}
|
||||
onInstall={handleInstallPlugin}
|
||||
tagNames={tagNames}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
117
web/src/app/home/plugins/components/plugin-market/TagsFilter.tsx
Normal file
117
web/src/app/home/plugins/components/plugin-market/TagsFilter.tsx
Normal file
@@ -0,0 +1,117 @@
|
||||
'use client';
|
||||
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectTrigger,
|
||||
} from '@/components/ui/select';
|
||||
import { Checkbox } from '@/components/ui/checkbox';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Tag as TagIcon } from 'lucide-react';
|
||||
import { useState } from 'react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { PluginTag } from '@/app/infra/http/CloudServiceClient';
|
||||
|
||||
interface TagsFilterProps {
|
||||
availableTags: PluginTag[];
|
||||
selectedTags: string[];
|
||||
onTagsChange: (tags: string[]) => void;
|
||||
}
|
||||
|
||||
export function TagsFilter({
|
||||
availableTags,
|
||||
selectedTags,
|
||||
onTagsChange,
|
||||
}: TagsFilterProps) {
|
||||
const { t, i18n } = useTranslation();
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const handleTagToggle = (tag: string) => {
|
||||
const newTags = selectedTags.includes(tag)
|
||||
? selectedTags.filter((t) => t !== tag)
|
||||
: [...selectedTags, tag];
|
||||
onTagsChange(newTags);
|
||||
};
|
||||
|
||||
const handleClearAll = () => {
|
||||
onTagsChange([]);
|
||||
};
|
||||
|
||||
const extractI18nObject = (obj: { zh_Hans?: string; en_US?: string }) => {
|
||||
const lang = i18n.language || 'en_US';
|
||||
return obj[lang as keyof typeof obj] || obj.zh_Hans || obj.en_US || '';
|
||||
};
|
||||
|
||||
return (
|
||||
<Select open={open} onOpenChange={setOpen}>
|
||||
<SelectTrigger className="w-[140px]">
|
||||
<div className="flex items-center gap-2 w-full">
|
||||
<TagIcon className="h-4 w-4 flex-shrink-0" />
|
||||
{selectedTags.length === 0 ? (
|
||||
<span className="text-muted-foreground truncate text-sm">
|
||||
{t('market.tags.filterByTags')}
|
||||
</span>
|
||||
) : (
|
||||
<span className="text-sm truncate">
|
||||
{selectedTags.length} {t('market.tags.selected')}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</SelectTrigger>
|
||||
<SelectContent className="w-[240px]">
|
||||
<SelectGroup>
|
||||
<div className="px-2 py-1.5 flex items-center justify-between border-b">
|
||||
<span className="text-sm font-medium">
|
||||
{t('market.tags.selectTags')}
|
||||
</span>
|
||||
{selectedTags.length > 0 && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={handleClearAll}
|
||||
className="h-auto p-0 text-xs hover:bg-transparent hover:text-destructive"
|
||||
>
|
||||
{t('market.tags.clearAll')}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{availableTags.length === 0 ? (
|
||||
<div className="px-2 py-6 text-center text-sm text-muted-foreground">
|
||||
{t('market.tags.noTags')}
|
||||
</div>
|
||||
) : (
|
||||
<div className="max-h-[300px] overflow-y-auto">
|
||||
{availableTags.map((tag) => (
|
||||
<div
|
||||
key={tag.tag}
|
||||
className="flex items-center space-x-2 px-2 py-2 hover:bg-accent cursor-pointer"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
handleTagToggle(tag.tag);
|
||||
}}
|
||||
>
|
||||
<Checkbox
|
||||
id={`tag-${tag.tag}`}
|
||||
checked={selectedTags.includes(tag.tag)}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
onCheckedChange={() => handleTagToggle(tag.tag)}
|
||||
/>
|
||||
<Label
|
||||
htmlFor={`tag-${tag.tag}`}
|
||||
className="text-sm font-normal cursor-pointer flex-1"
|
||||
onClick={(e) => e.preventDefault()}
|
||||
>
|
||||
{extractI18nObject(tag.display_name)}
|
||||
</Label>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
);
|
||||
}
|
||||
@@ -15,9 +15,11 @@ import { Button } from '@/components/ui/button';
|
||||
export default function PluginMarketCardComponent({
|
||||
cardVO,
|
||||
onInstall,
|
||||
tagNames = {},
|
||||
}: {
|
||||
cardVO: PluginMarketCardVO;
|
||||
onInstall?: (author: string, pluginName: string) => void;
|
||||
tagNames?: Record<string, string>;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const [isHovered, setIsHovered] = useState(false);
|
||||
@@ -42,13 +44,6 @@ export default function PluginMarketCardComponent({
|
||||
KnowledgeRetriever: <Book className="w-4 h-4" />,
|
||||
};
|
||||
|
||||
const componentKindNameMap: Record<string, string> = {
|
||||
Tool: t('plugins.componentName.Tool'),
|
||||
EventListener: t('plugins.componentName.EventListener'),
|
||||
Command: t('plugins.componentName.Command'),
|
||||
KnowledgeRetriever: t('plugins.componentName.KnowledgeRetriever'),
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className="w-[100%] h-auto min-h-[8rem] sm:min-h-[9rem] bg-white rounded-[10px] shadow-[0px_0px_4px_0_rgba(0,0,0,0.2)] p-3 sm:p-[1rem] hover:shadow-[0px_3px_6px_0_rgba(0,0,0,0.12)] transition-all duration-200 hover:scale-[1.005] dark:bg-[#1f1f22] relative"
|
||||
@@ -97,11 +92,13 @@ export default function PluginMarketCardComponent({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 下部分:下载量和组件列表 */}
|
||||
<div className="w-full flex flex-row items-center justify-between gap-[0.3rem] sm:gap-[0.4rem] px-0 sm:px-[0.4rem] flex-shrink-0">
|
||||
<div className="flex flex-row items-center justify-start gap-[0.3rem] sm:gap-[0.4rem]">
|
||||
{/* 下部分:下载量、标签和组件列表 */}
|
||||
<div className="w-full flex flex-row items-center justify-between gap-2 px-0 sm:px-[0.4rem] flex-shrink-0">
|
||||
<div className="flex flex-row items-center justify-start gap-2 flex-wrap">
|
||||
{/* 下载数量 */}
|
||||
<div className="flex flex-row items-center gap-[0.3rem] sm:gap-[0.4rem]">
|
||||
<svg
|
||||
className="w-4 h-4 sm:w-[1.2rem] sm:h-[1.2rem] text-[#2563eb] flex-shrink-0"
|
||||
className="w-4 h-4 sm:w-[1.2rem] sm:h-[1.2rem] text-[#2563eb] dark:text-[#5b8def] flex-shrink-0"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
@@ -112,11 +109,48 @@ export default function PluginMarketCardComponent({
|
||||
<polyline points="7,10 12,15 17,10" />
|
||||
<line x1="12" y1="15" x2="12" y2="3" />
|
||||
</svg>
|
||||
<div className="text-xs sm:text-sm text-[#2563eb] font-medium whitespace-nowrap">
|
||||
<div className="text-xs sm:text-sm text-[#2563eb] dark:text-[#5b8def] font-medium whitespace-nowrap">
|
||||
{cardVO.installCount.toLocaleString()}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Tags */}
|
||||
{cardVO.tags && cardVO.tags.length > 0 && (
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{cardVO.tags.slice(0, 2).map((tag) => (
|
||||
<Badge
|
||||
key={tag}
|
||||
variant="secondary"
|
||||
className="text-[0.65rem] sm:text-[0.7rem] px-2 py-0.5 h-5 flex items-center gap-1 flex-shrink-0"
|
||||
>
|
||||
<svg
|
||||
className="w-2.5 h-2.5 flex-shrink-0"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<path d="M20.59 13.41l-7.17 7.17a2 2 0 0 1-2.83 0L2 12V2h10l8.59 8.59a2 2 0 0 1 0 2.82z" />
|
||||
<line x1="7" y1="7" x2="7.01" y2="7" />
|
||||
</svg>
|
||||
<span className="truncate">{tagNames[tag] || tag}</span>
|
||||
</Badge>
|
||||
))}
|
||||
{cardVO.tags.length > 2 && (
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="text-[0.65rem] sm:text-[0.7rem] px-2 py-0.5 h-5 flex items-center flex-shrink-0"
|
||||
>
|
||||
+{cardVO.tags.length - 2}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 组件列表 */}
|
||||
{cardVO.components && Object.keys(cardVO.components).length > 0 && (
|
||||
<div className="flex flex-row items-center gap-1">
|
||||
@@ -127,10 +161,6 @@ export default function PluginMarketCardComponent({
|
||||
className="flex items-center gap-1"
|
||||
>
|
||||
{kindIconMap[kind]}
|
||||
{/* 响应式显示组件名称:在中等屏幕以上显示 */}
|
||||
<span className="hidden md:inline">
|
||||
{componentKindNameMap[kind]}
|
||||
</span>
|
||||
<span className="ml-1">{count}</span>
|
||||
</Badge>
|
||||
))}
|
||||
|
||||
@@ -9,6 +9,7 @@ export interface IPluginMarketCardVO {
|
||||
githubURL: string;
|
||||
version: string;
|
||||
components?: Record<string, number>;
|
||||
tags?: string[];
|
||||
}
|
||||
|
||||
export class PluginMarketCardVO implements IPluginMarketCardVO {
|
||||
@@ -22,6 +23,7 @@ export class PluginMarketCardVO implements IPluginMarketCardVO {
|
||||
installCount: number;
|
||||
version: string;
|
||||
components?: Record<string, number>;
|
||||
tags?: string[];
|
||||
|
||||
constructor(prop: IPluginMarketCardVO) {
|
||||
this.description = prop.description;
|
||||
@@ -34,5 +36,6 @@ export class PluginMarketCardVO implements IPluginMarketCardVO {
|
||||
this.pluginId = prop.pluginId;
|
||||
this.version = prop.version;
|
||||
this.components = prop.components;
|
||||
this.tags = prop.tags;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,6 +35,7 @@ export class CloudServiceClient extends BaseHttpClient {
|
||||
sort_by?: string,
|
||||
sort_order?: string,
|
||||
component_filter?: string,
|
||||
tags_filter?: string[],
|
||||
): Promise<ApiRespMarketplacePlugins> {
|
||||
return this.post<ApiRespMarketplacePlugins>(
|
||||
'/api/v1/marketplace/plugins/search',
|
||||
@@ -45,6 +46,7 @@ export class CloudServiceClient extends BaseHttpClient {
|
||||
sort_by,
|
||||
sort_order,
|
||||
component_filter,
|
||||
tags_filter,
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -92,6 +94,20 @@ export class CloudServiceClient extends BaseHttpClient {
|
||||
public getLangBotReleases(): Promise<GitHubRelease[]> {
|
||||
return this.get<GitHubRelease[]>('/api/v1/dist/info/releases');
|
||||
}
|
||||
|
||||
public getAllTags(): Promise<{ tags: PluginTag[] }> {
|
||||
return this.get<{ tags: PluginTag[] }>('/api/v1/marketplace/tags');
|
||||
}
|
||||
}
|
||||
|
||||
export interface PluginTag {
|
||||
tag: string;
|
||||
display_name: {
|
||||
zh_Hans?: string;
|
||||
en_US?: string;
|
||||
zh_Hant?: string;
|
||||
ja_JP?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface GitHubRelease {
|
||||
|
||||
@@ -446,7 +446,7 @@ const enUS = {
|
||||
downloadFailed: 'Download failed',
|
||||
noReadme: 'This plugin does not provide README documentation',
|
||||
description: 'Description',
|
||||
tags: 'Tags',
|
||||
tagLabel: 'Tags',
|
||||
submissionTitle: 'You have a plugin submission under review: {{name}}',
|
||||
submissionPending: 'Your plugin submission is under review: {{name}}',
|
||||
submissionApproved: 'Your plugin submission has been approved: {{name}}',
|
||||
@@ -462,6 +462,13 @@ const enUS = {
|
||||
allComponents: 'All Components',
|
||||
requestPlugin: 'Request Plugin',
|
||||
viewDetails: 'View Details',
|
||||
tags: {
|
||||
filterByTags: 'Filter by Tags',
|
||||
selected: 'selected',
|
||||
selectTags: 'Select Tags',
|
||||
clearAll: 'Clear All',
|
||||
noTags: 'No tags available',
|
||||
},
|
||||
},
|
||||
mcp: {
|
||||
title: 'MCP',
|
||||
|
||||
@@ -447,7 +447,7 @@ const jaJP = {
|
||||
downloadFailed: 'ダウンロード失敗',
|
||||
noReadme: 'このプラグインはREADMEドキュメントを提供していません',
|
||||
description: '説明',
|
||||
tags: 'タグ',
|
||||
tagLabel: 'タグ',
|
||||
submissionTitle: 'プラグインの提出が審査中です: {{name}}',
|
||||
submissionPending: 'プラグインの提出が審査中です: {{name}}',
|
||||
submissionApproved: 'プラグインの提出が承認されました: {{name}}',
|
||||
@@ -462,6 +462,13 @@ const jaJP = {
|
||||
filterByComponent: 'コンポーネント',
|
||||
allComponents: '全部コンポーネント',
|
||||
requestPlugin: 'プラグインをリクエスト',
|
||||
tags: {
|
||||
filterByTags: 'タグで絞り込み',
|
||||
selected: '選択済み',
|
||||
selectTags: 'タグを選択',
|
||||
clearAll: 'クリア',
|
||||
noTags: 'タグがありません',
|
||||
},
|
||||
viewDetails: '詳細を表示',
|
||||
},
|
||||
mcp: {
|
||||
|
||||
@@ -425,7 +425,7 @@ const zhHans = {
|
||||
downloadFailed: '下载失败',
|
||||
noReadme: '该插件没有提供 README 文档',
|
||||
description: '描述',
|
||||
tags: '标签',
|
||||
tagLabel: '标签',
|
||||
submissionTitle: '您有插件提交正在审核中: {{name}}',
|
||||
submissionApproved: '您的插件提交已通过审核: {{name}}',
|
||||
submissionRejected: '您的插件提交已被拒绝: {{name}}',
|
||||
@@ -439,6 +439,13 @@ const zhHans = {
|
||||
filterByComponent: '组件',
|
||||
allComponents: '全部组件',
|
||||
requestPlugin: '请求插件',
|
||||
tags: {
|
||||
filterByTags: '按标签筛选',
|
||||
selected: '已选',
|
||||
selectTags: '选择标签',
|
||||
clearAll: '清空',
|
||||
noTags: '暂无标签',
|
||||
},
|
||||
viewDetails: '查看详情',
|
||||
},
|
||||
mcp: {
|
||||
|
||||
@@ -418,7 +418,7 @@ const zhHant = {
|
||||
downloadFailed: '下載失敗',
|
||||
noReadme: '該插件沒有提供 README 文件',
|
||||
description: '描述',
|
||||
tags: '標籤',
|
||||
tagLabel: '標籤',
|
||||
submissionTitle: '您有插件提交正在審核中: {{name}}',
|
||||
submissionApproved: '您的插件提交已通過審核: {{name}}',
|
||||
submissionRejected: '您的插件提交已被拒絕: {{name}}',
|
||||
@@ -432,6 +432,13 @@ const zhHant = {
|
||||
filterByComponent: '組件',
|
||||
allComponents: '全部組件',
|
||||
requestPlugin: '請求插件',
|
||||
tags: {
|
||||
filterByTags: '按標籤篩選',
|
||||
selected: '已選',
|
||||
selectTags: '選擇標籤',
|
||||
clearAll: '清空',
|
||||
noTags: '暫無標籤',
|
||||
},
|
||||
viewDetails: '查看詳情',
|
||||
},
|
||||
mcp: {
|
||||
|
||||
Reference in New Issue
Block a user