feat(ui): i18n for claude code plugins

This commit is contained in:
吹雪 2026-06-11 05:28:18 +09:00
parent 3ff23a2cb2
commit 7ddaa3a005
6 changed files with 403 additions and 106 deletions

View file

@ -1,6 +1,7 @@
import React, { useState, useEffect } from "react";
import { Modal, Form, Steps, Button, Checkbox } from "antd";
import { Text, Title, Badge } from "@tremor/react";
import { useTranslation, Trans } from "react-i18next";
import { enableClaudeCodePlugin, disableClaudeCodePlugin } from "../networking";
import NotificationsManager from "../molecules/notifications_manager";
import { Plugin } from "./types";
@ -22,6 +23,7 @@ const MakeSkillPublicForm: React.FC<MakeSkillPublicFormProps> = ({
skillsList,
onSuccess,
}) => {
const { t } = useTranslation();
const [currentStep, setCurrentStep] = useState(0);
const [selectedSkills, setSelectedSkills] = useState<Set<string>>(new Set());
const [loading, setLoading] = useState(false);
@ -36,7 +38,7 @@ const MakeSkillPublicForm: React.FC<MakeSkillPublicFormProps> = ({
const handleNext = () => {
if (selectedSkills.size === 0) {
NotificationsManager.fromBackend("Please select at least one skill");
NotificationsManager.fromBackend(t("claudeCodePluginsPage.makeSkillPublicForm.selectAtLeastOne"));
return;
}
setCurrentStep(1);
@ -69,7 +71,7 @@ const MakeSkillPublicForm: React.FC<MakeSkillPublicFormProps> = ({
const handleSubmit = async () => {
if (selectedSkills.size === 0) {
NotificationsManager.fromBackend("Please select at least one skill");
NotificationsManager.fromBackend(t("claudeCodePluginsPage.makeSkillPublicForm.selectAtLeastOne"));
return;
}
@ -89,12 +91,14 @@ const MakeSkillPublicForm: React.FC<MakeSkillPublicFormProps> = ({
}),
);
NotificationsManager.success(`Skill Hub updated — ${selectedSkills.size} skill(s) published`);
NotificationsManager.success(
t("claudeCodePluginsPage.makeSkillPublicForm.hubUpdated", { count: selectedSkills.size }),
);
handleClose();
onSuccess();
} catch (error) {
console.error("Error publishing skills:", error);
NotificationsManager.fromBackend("Failed to update skills. Please try again.");
NotificationsManager.fromBackend(t("claudeCodePluginsPage.makeSkillPublicForm.updateFailed"));
} finally {
setLoading(false);
}
@ -106,26 +110,24 @@ const MakeSkillPublicForm: React.FC<MakeSkillPublicFormProps> = ({
const renderStep1 = () => (
<div className="space-y-4">
<div className="flex items-center justify-between">
<Title>Select Skills to Publish</Title>
<Title>{t("claudeCodePluginsPage.makeSkillPublicForm.selectSkillsTitle")}</Title>
<Checkbox
checked={allSelected}
indeterminate={isIndeterminate}
onChange={(e) => handleSelectAll(e.target.checked)}
disabled={skillsList.length === 0}
>
Select All ({skillsList.length})
{t("claudeCodePluginsPage.makeSkillPublicForm.selectAll", { count: skillsList.length })}
</Checkbox>
</div>
<Text className="text-sm text-gray-600">
Selected skills will be visible to all users in the Skill Hub. Deselected skills will be unpublished.
</Text>
<Text className="text-sm text-gray-600">{t("claudeCodePluginsPage.makeSkillPublicForm.visibilityNote")}</Text>
<div className="max-h-96 overflow-y-auto border rounded-lg p-4">
<div className="space-y-3">
{skillsList.length === 0 ? (
<div className="text-center py-8 text-gray-500">
<Text>No skills registered yet.</Text>
<Text>{t("claudeCodePluginsPage.makeSkillPublicForm.noSkillsRegistered")}</Text>
</div>
) : (
skillsList.map((skill) => (
@ -139,7 +141,7 @@ const MakeSkillPublicForm: React.FC<MakeSkillPublicFormProps> = ({
<Text className="font-medium font-mono text-sm">{skill.name}</Text>
{skill.enabled && (
<Badge color="green" size="xs">
Public
{t("claudeCodePluginsPage.makeSkillPublicForm.publicBadge")}
</Badge>
)}
</div>
@ -161,7 +163,11 @@ const MakeSkillPublicForm: React.FC<MakeSkillPublicFormProps> = ({
{selectedSkills.size > 0 && (
<div className="bg-blue-50 border border-blue-200 rounded-lg p-3">
<Text className="text-sm text-blue-800">
<strong>{selectedSkills.size}</strong> skill{selectedSkills.size !== 1 ? "s" : ""} will be published
<Trans
i18nKey="claudeCodePluginsPage.makeSkillPublicForm.willBePublished"
count={selectedSkills.size}
components={{ strong: <strong /> }}
/>
</Text>
</div>
)}
@ -170,17 +176,16 @@ const MakeSkillPublicForm: React.FC<MakeSkillPublicFormProps> = ({
const renderStep2 = () => (
<div className="space-y-4">
<Title>Confirm Publish to Skill Hub</Title>
<Title>{t("claudeCodePluginsPage.makeSkillPublicForm.confirmTitle")}</Title>
<div className="bg-yellow-50 border border-yellow-200 rounded-lg p-4">
<Text className="text-sm text-yellow-800">
<strong>Note:</strong> Published skills will be visible to all users in the Skill Hub tab. Skills not in the
list below will be unpublished.
<Trans i18nKey="claudeCodePluginsPage.makeSkillPublicForm.confirmNote" components={{ strong: <strong /> }} />
</Text>
</div>
<div className="space-y-3">
<Text className="font-medium">Skills to be published:</Text>
<Text className="font-medium">{t("claudeCodePluginsPage.makeSkillPublicForm.skillsToBePublished")}</Text>
<div className="max-h-48 overflow-y-auto border rounded-lg p-3">
<div className="space-y-2">
{Array.from(selectedSkills).map((name) => {
@ -202,7 +207,11 @@ const MakeSkillPublicForm: React.FC<MakeSkillPublicFormProps> = ({
<div className="bg-blue-50 border border-blue-200 rounded-lg p-3">
<Text className="text-sm text-blue-800">
Total: <strong>{selectedSkills.size}</strong> skill{selectedSkills.size !== 1 ? "s" : ""} will be published
<Trans
i18nKey="claudeCodePluginsPage.makeSkillPublicForm.totalWillBePublished"
count={selectedSkills.size}
components={{ strong: <strong /> }}
/>
</Text>
</div>
</div>
@ -210,7 +219,7 @@ const MakeSkillPublicForm: React.FC<MakeSkillPublicFormProps> = ({
return (
<Modal
title="Publish to Skill Hub"
title={t("claudeCodePluginsPage.makeSkillPublicForm.modalTitle")}
open={visible}
onCancel={handleClose}
footer={null}
@ -219,25 +228,25 @@ const MakeSkillPublicForm: React.FC<MakeSkillPublicFormProps> = ({
>
<Form form={form} layout="vertical">
<Steps current={currentStep} className="mb-6">
<Step title="Select Skills" />
<Step title="Confirm" />
<Step title={t("claudeCodePluginsPage.makeSkillPublicForm.stepSelectSkills")} />
<Step title={t("common.confirm")} />
</Steps>
{currentStep === 0 ? renderStep1() : renderStep2()}
<div className="flex justify-between mt-6">
<Button onClick={currentStep === 0 ? handleClose : () => setCurrentStep(0)}>
{currentStep === 0 ? "Cancel" : "Previous"}
{currentStep === 0 ? t("common.cancel") : t("common.previous")}
</Button>
<div className="flex space-x-2">
{currentStep === 0 && (
<Button onClick={handleNext} disabled={selectedSkills.size === 0}>
Next
{t("common.next")}
</Button>
)}
{currentStep === 1 && (
<Button onClick={handleSubmit} loading={loading}>
Publish to Hub
{t("claudeCodePluginsPage.makeSkillPublicForm.publishToHub")}
</Button>
)}
</div>

View file

@ -1,5 +1,6 @@
import React, { useState } from "react";
import { Modal, Form, Input, Select } from "antd";
import { useTranslation } from "react-i18next";
import MessageManager from "@/components/molecules/message_manager";
import { Button } from "@tremor/react";
import { registerClaudeCodePlugin } from "../networking";
@ -101,6 +102,7 @@ function parseGitHubUrl(raw: string): ParsePreview | null {
}
const AddPluginForm: React.FC<AddPluginFormProps> = ({ visible, onClose, accessToken, onSuccess }) => {
const { t } = useTranslation();
const [form] = Form.useForm();
const [isSubmitting, setIsSubmitting] = useState(false);
const [urlPreview, setUrlPreview] = useState<ParsePreview | null>(null);
@ -120,32 +122,32 @@ const AddPluginForm: React.FC<AddPluginFormProps> = ({ visible, onClose, accessT
const handleSubmit = async (values: any) => {
if (!accessToken) {
MessageManager.error("No access token available");
MessageManager.error(t("claudeCodePluginsPage.addPluginForm.noAccessToken"));
return;
}
if (!urlPreview) {
MessageManager.error("Please enter a valid GitHub URL");
MessageManager.error(t("claudeCodePluginsPage.addPluginForm.enterValidGithubUrl"));
return;
}
if (!validatePluginName(values.name)) {
MessageManager.error("Skill name must be kebab-case (lowercase letters, numbers, and hyphens only)");
MessageManager.error(t("claudeCodePluginsPage.addPluginForm.invalidSkillName"));
return;
}
if (values.version && !isValidSemanticVersion(values.version)) {
MessageManager.error("Version must be in semantic versioning format (e.g., 1.0.0)");
MessageManager.error(t("claudeCodePluginsPage.addPluginForm.invalidVersion"));
return;
}
if (values.authorEmail && !isValidEmail(values.authorEmail)) {
MessageManager.error("Invalid email format");
MessageManager.error(t("claudeCodePluginsPage.addPluginForm.invalidEmail"));
return;
}
if (values.homepage && !isValidUrl(values.homepage)) {
MessageManager.error("Invalid homepage URL format");
MessageManager.error(t("claudeCodePluginsPage.addPluginForm.invalidHomepageUrl"));
return;
}
@ -170,14 +172,14 @@ const AddPluginForm: React.FC<AddPluginFormProps> = ({ visible, onClose, accessT
if (values.namespace) pluginData.namespace = values.namespace.trim();
await registerClaudeCodePlugin(accessToken, pluginData);
MessageManager.success("Skill registered successfully");
MessageManager.success(t("claudeCodePluginsPage.addPluginForm.registerSuccess"));
form.resetFields();
setUrlPreview(null);
onSuccess();
onClose();
} catch (error) {
console.error("Error registering skill:", error);
MessageManager.error("Failed to register skill");
MessageManager.error(t("claudeCodePluginsPage.addPluginForm.registerFailed"));
} finally {
setIsSubmitting(false);
}
@ -190,14 +192,21 @@ const AddPluginForm: React.FC<AddPluginFormProps> = ({ visible, onClose, accessT
};
return (
<Modal title="Add New Skill" open={visible} onCancel={handleCancel} footer={null} width={700} className="top-8">
<Modal
title={t("claudeCodePluginsPage.addPluginForm.modalTitle")}
open={visible}
onCancel={handleCancel}
footer={null}
width={700}
className="top-8"
>
<Form form={form} layout="vertical" onFinish={handleSubmit} className="mt-4">
{/* Smart URL Input */}
<Form.Item
label="GitHub URL"
label={t("claudeCodePluginsPage.addPluginForm.githubUrlLabel")}
name="skillUrl"
rules={[{ required: true, message: "Please enter a GitHub URL" }]}
tooltip="Paste a GitHub URL — repo, folder, or file link. E.g. github.com/org/repo or github.com/org/repo/tree/main/my-skill"
rules={[{ required: true, message: t("claudeCodePluginsPage.addPluginForm.githubUrlRequired") }]}
tooltip={t("claudeCodePluginsPage.addPluginForm.githubUrlTooltip")}
>
<Input
placeholder="https://github.com/org/repo/tree/main/my-skill"
@ -209,22 +218,22 @@ const AddPluginForm: React.FC<AddPluginFormProps> = ({ visible, onClose, accessT
{/* Parsed preview */}
{urlPreview && (
<div className="mb-4 px-3 py-2 bg-blue-50 border border-blue-200 rounded-lg text-sm text-blue-700">
Detected: {urlPreview.label}
{t("claudeCodePluginsPage.addPluginForm.detected", { label: urlPreview.label })}
</div>
)}
{/* Skill Name */}
<Form.Item
label="Skill Name"
label={t("claudeCodePluginsPage.addPluginForm.skillNameLabel")}
name="name"
rules={[
{ required: true, message: "Please enter skill name" },
{ required: true, message: t("claudeCodePluginsPage.addPluginForm.skillNameRequired") },
{
pattern: /^[a-z0-9-]+$/,
message: "Name must be kebab-case (lowercase, numbers, hyphens only)",
message: t("claudeCodePluginsPage.addPluginForm.skillNamePattern"),
},
]}
tooltip="Unique identifier in kebab-case format (e.g., my-skill)"
tooltip={t("claudeCodePluginsPage.addPluginForm.skillNameTooltip")}
>
<Input placeholder="my-skill" className="rounded-lg" />
</Form.Item>
@ -232,32 +241,45 @@ const AddPluginForm: React.FC<AddPluginFormProps> = ({ visible, onClose, accessT
{/* Domain and Namespace — side by side */}
<div className="flex gap-4">
<Form.Item
label="Domain (Optional)"
label={t("claudeCodePluginsPage.addPluginForm.domainLabel")}
name="domain"
tooltip="Top-level grouping in the Skill Hub (e.g., Productivity)"
tooltip={t("claudeCodePluginsPage.addPluginForm.domainTooltip")}
className="flex-1"
>
<Input placeholder="Productivity" className="rounded-lg" />
<Input placeholder={t("claudeCodePluginsPage.addPluginForm.domainPlaceholder")} className="rounded-lg" />
</Form.Item>
<Form.Item
label="Namespace (Optional)"
label={t("claudeCodePluginsPage.addPluginForm.namespaceLabel")}
name="namespace"
tooltip="Sub-grouping within domain (e.g., workflows)"
tooltip={t("claudeCodePluginsPage.addPluginForm.namespaceTooltip")}
className="flex-1"
>
<Input placeholder="workflows" className="rounded-lg" />
<Input placeholder={t("claudeCodePluginsPage.addPluginForm.namespacePlaceholder")} className="rounded-lg" />
</Form.Item>
</div>
{/* Description */}
<Form.Item label="Description (Optional)" name="description" tooltip="Brief description of what the skill does">
<TextArea rows={3} placeholder="A skill that helps with..." maxLength={500} className="rounded-lg" />
<Form.Item
label={t("claudeCodePluginsPage.addPluginForm.descriptionLabel")}
name="description"
tooltip={t("claudeCodePluginsPage.addPluginForm.descriptionTooltip")}
>
<TextArea
rows={3}
placeholder={t("claudeCodePluginsPage.addPluginForm.descriptionPlaceholder")}
maxLength={500}
className="rounded-lg"
/>
</Form.Item>
{/* Category */}
<Form.Item label="Category (Optional)" name="category" tooltip="Select a category or enter a custom one">
<Form.Item
label={t("claudeCodePluginsPage.addPluginForm.categoryLabel")}
name="category"
tooltip={t("claudeCodePluginsPage.addPluginForm.categoryTooltip")}
>
<Select
placeholder="Select or type a category"
placeholder={t("claudeCodePluginsPage.addPluginForm.categoryPlaceholder")}
allowClear
showSearch
optionFilterProp="children"
@ -272,26 +294,38 @@ const AddPluginForm: React.FC<AddPluginFormProps> = ({ visible, onClose, accessT
</Form.Item>
{/* Keywords */}
<Form.Item label="Keywords (Optional)" name="keywords" tooltip="Comma-separated list of keywords for search">
<Input placeholder="search, web, api" className="rounded-lg" />
<Form.Item
label={t("claudeCodePluginsPage.addPluginForm.keywordsLabel")}
name="keywords"
tooltip={t("claudeCodePluginsPage.addPluginForm.keywordsTooltip")}
>
<Input placeholder={t("claudeCodePluginsPage.addPluginForm.keywordsPlaceholder")} className="rounded-lg" />
</Form.Item>
{/* Version */}
<Form.Item label="Version (Optional)" name="version" tooltip="Semantic version (e.g., 1.0.0)">
<Form.Item
label={t("claudeCodePluginsPage.addPluginForm.versionLabel")}
name="version"
tooltip={t("claudeCodePluginsPage.addPluginForm.versionTooltip")}
>
<Input placeholder="1.0.0" className="rounded-lg" />
</Form.Item>
{/* Author Name */}
<Form.Item label="Author Name (Optional)" name="authorName" tooltip="Name of the skill author or organization">
<Input placeholder="Your Name or Organization" className="rounded-lg" />
<Form.Item
label={t("claudeCodePluginsPage.addPluginForm.authorNameLabel")}
name="authorName"
tooltip={t("claudeCodePluginsPage.addPluginForm.authorNameTooltip")}
>
<Input placeholder={t("claudeCodePluginsPage.addPluginForm.authorNamePlaceholder")} className="rounded-lg" />
</Form.Item>
{/* Author Email */}
<Form.Item
label="Author Email (Optional)"
label={t("claudeCodePluginsPage.addPluginForm.authorEmailLabel")}
name="authorEmail"
rules={[{ type: "email", message: "Please enter a valid email" }]}
tooltip="Contact email for the skill author"
rules={[{ type: "email", message: t("claudeCodePluginsPage.addPluginForm.authorEmailRule") }]}
tooltip={t("claudeCodePluginsPage.addPluginForm.authorEmailTooltip")}
>
<Input type="email" placeholder="author@example.com" className="rounded-lg" />
</Form.Item>
@ -300,10 +334,12 @@ const AddPluginForm: React.FC<AddPluginFormProps> = ({ visible, onClose, accessT
<Form.Item className="mb-0 mt-6">
<div className="flex justify-end gap-2">
<Button variant="secondary" onClick={handleCancel} disabled={isSubmitting}>
Cancel
{t("common.cancel")}
</Button>
<Button type="submit" loading={isSubmitting}>
{isSubmitting ? "Adding..." : "Add Skill"}
{isSubmitting
? t("claudeCodePluginsPage.addPluginForm.adding")
: t("claudeCodePluginsPage.addPluginForm.addSkill")}
</Button>
</div>
</Form.Item>

View file

@ -11,6 +11,7 @@ import {
import { Badge, Button, Table, TableBody, TableCell, TableHead, TableHeaderCell, TableRow } from "@tremor/react";
import { Tooltip } from "antd";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import NotificationsManager from "../molecules/notifications_manager";
import { getCategoryBadgeColor } from "./helpers";
import { Plugin } from "./types";
@ -32,6 +33,7 @@ const PluginTable: React.FC<PluginTableProps> = ({
isAdmin,
onPluginClick,
}) => {
const { t } = useTranslation();
const [sorting, setSorting] = useState<SortingState>([{ id: "created_at", desc: true }]);
const formatDate = (dateString?: string) => {
@ -42,12 +44,12 @@ const PluginTable: React.FC<PluginTableProps> = ({
const copyToClipboard = (text: string) => {
navigator.clipboard.writeText(text);
NotificationsManager.success("Copied to clipboard!");
NotificationsManager.success(t("claudeCodePluginsPage.pluginTable.copiedToClipboard"));
};
const columns: ColumnDef<Plugin>[] = [
{
header: "Skill Name",
header: t("claudeCodePluginsPage.pluginTable.colSkillName"),
accessorKey: "name",
cell: ({ row }) => {
const plugin = row.original;
@ -64,7 +66,7 @@ const PluginTable: React.FC<PluginTableProps> = ({
{name}
</Button>
</Tooltip>
<Tooltip title="Copy Plugin ID">
<Tooltip title={t("claudeCodePluginsPage.pluginTable.copyPluginId")}>
<CopyOutlined
onClick={(e) => {
e.stopPropagation();
@ -78,18 +80,18 @@ const PluginTable: React.FC<PluginTableProps> = ({
},
},
{
header: "Version",
header: t("claudeCodePluginsPage.pluginTable.colVersion"),
accessorKey: "version",
cell: ({ row }) => {
const version = row.original.version || "N/A";
const version = row.original.version || t("claudeCodePluginsPage.pluginTable.versionNA");
return <span className="text-xs text-gray-600">{version}</span>;
},
},
{
header: "Description",
header: t("common.description"),
accessorKey: "description",
cell: ({ row }) => {
const description = row.original.description || "No description";
const description = row.original.description || t("claudeCodePluginsPage.pluginTable.noDescription");
return (
<Tooltip title={description}>
<span className="text-xs text-gray-600 block max-w-[300px] truncate">{description}</span>
@ -98,14 +100,14 @@ const PluginTable: React.FC<PluginTableProps> = ({
},
},
{
header: "Category",
header: t("claudeCodePluginsPage.pluginTable.colCategory"),
accessorKey: "category",
cell: ({ row }) => {
const category = row.original.category;
if (!category) {
return (
<Badge color="gray" className="text-xs font-normal" size="xs">
Uncategorized
{t("claudeCodePluginsPage.pluginTable.uncategorized")}
</Badge>
);
}
@ -118,19 +120,19 @@ const PluginTable: React.FC<PluginTableProps> = ({
},
},
{
header: "Public",
header: t("claudeCodePluginsPage.pluginTable.colPublic"),
accessorKey: "enabled",
cell: ({ row }) => {
const plugin = row.original;
return (
<Badge color={plugin.enabled ? "green" : "gray"} className="text-xs font-normal" size="xs">
{plugin.enabled ? "Yes" : "No"}
{plugin.enabled ? t("common.yes") : t("common.no")}
</Badge>
);
},
},
{
header: "Created At",
header: t("common.createdAt"),
accessorKey: "created_at",
cell: ({ row }) => {
const plugin = row.original;
@ -144,7 +146,7 @@ const PluginTable: React.FC<PluginTableProps> = ({
...(isAdmin
? [
{
header: "Actions",
header: t("common.actions"),
id: "actions",
enableSorting: false,
cell: ({ row }: any) => {
@ -152,7 +154,7 @@ const PluginTable: React.FC<PluginTableProps> = ({
return (
<div className="flex items-center gap-1">
<Tooltip title="Delete skill">
<Tooltip title={t("claudeCodePluginsPage.pluginTable.deleteSkill")}>
<Button
size="xs"
variant="light"
@ -227,7 +229,7 @@ const PluginTable: React.FC<PluginTableProps> = ({
<TableRow>
<TableCell colSpan={columns.length} className="h-8 text-center">
<div className="text-center text-gray-500">
<p>Loading...</p>
<p>{t("common.loading")}</p>
</div>
</TableCell>
</TableRow>
@ -256,7 +258,7 @@ const PluginTable: React.FC<PluginTableProps> = ({
<TableRow>
<TableCell colSpan={columns.length} className="h-8 text-center">
<div className="text-center text-gray-500">
<p>No skills found. Add one to get started.</p>
<p>{t("claudeCodePluginsPage.pluginTable.noSkillsFound")}</p>
</div>
</TableCell>
</TableRow>

View file

@ -1,5 +1,6 @@
import React, { useState } from "react";
import { ArrowLeftOutlined, CopyOutlined, CheckOutlined, LinkOutlined } from "@ant-design/icons";
import { useTranslation, Trans } from "react-i18next";
import { formatInstallCommand } from "./helpers";
import { Plugin } from "./types";
@ -12,6 +13,7 @@ interface SkillDetailProps {
}
const SkillDetail: React.FC<SkillDetailProps> = ({ skill, onBack }) => {
const { t } = useTranslation();
const [activeTab, setActiveTab] = useState("overview");
const [copiedKey, setCopiedKey] = useState<string | null>(null);
@ -32,17 +34,30 @@ const SkillDetail: React.FC<SkillDetailProps> = ({ skill, onBack }) => {
const installCommand = formatInstallCommand(skill);
const detailRows = [
...(skill.category ? [{ property: "Category", value: skill.category }] : []),
...(skill.domain ? [{ property: "Domain", value: skill.domain }] : []),
...(skill.namespace ? [{ property: "Namespace", value: skill.namespace }] : []),
...(skill.version ? [{ property: "Version", value: skill.version }] : []),
...(skill.author?.name ? [{ property: "Author", value: skill.author.name }] : []),
...(skill.created_at ? [{ property: "Added", value: new Date(skill.created_at).toLocaleDateString() }] : []),
...(skill.category
? [{ property: t("claudeCodePluginsPage.skillDetail.propCategory"), value: skill.category }]
: []),
...(skill.domain ? [{ property: t("claudeCodePluginsPage.skillDetail.propDomain"), value: skill.domain }] : []),
...(skill.namespace
? [{ property: t("claudeCodePluginsPage.skillDetail.propNamespace"), value: skill.namespace }]
: []),
...(skill.version ? [{ property: t("claudeCodePluginsPage.skillDetail.propVersion"), value: skill.version }] : []),
...(skill.author?.name
? [{ property: t("claudeCodePluginsPage.skillDetail.propAuthor"), value: skill.author.name }]
: []),
...(skill.created_at
? [
{
property: t("claudeCodePluginsPage.skillDetail.propAdded"),
value: new Date(skill.created_at).toLocaleDateString(),
},
]
: []),
];
const tabs = [
{ key: "overview", label: "Overview" },
{ key: "usage", label: "How to Use" },
{ key: "overview", label: t("claudeCodePluginsPage.skillDetail.tabOverview") },
{ key: "usage", label: t("claudeCodePluginsPage.skillDetail.tabUsage") },
];
return (
@ -61,7 +76,7 @@ const SkillDetail: React.FC<SkillDetailProps> = ({ skill, onBack }) => {
}}
>
<ArrowLeftOutlined style={{ fontSize: 11 }} />
<span>Skills</span>
<span>{t("claudeCodePluginsPage.skillDetail.backLink")}</span>
</div>
{/* Header */}
@ -100,13 +115,17 @@ const SkillDetail: React.FC<SkillDetailProps> = ({ skill, onBack }) => {
<div style={{ display: "flex", gap: 64 }}>
{/* Left column */}
<div style={{ flex: 1, minWidth: 0 }}>
<h2 style={{ fontSize: 18, fontWeight: 400, color: "#202124", margin: "0 0 4px 0" }}>Skill Details</h2>
<p style={{ fontSize: 13, color: "#5f6368", margin: "0 0 16px 0" }}>Metadata registered with this skill</p>
<h2 style={{ fontSize: 18, fontWeight: 400, color: "#202124", margin: "0 0 4px 0" }}>
{t("claudeCodePluginsPage.skillDetail.skillDetailsHeading")}
</h2>
<p style={{ fontSize: 13, color: "#5f6368", margin: "0 0 16px 0" }}>
{t("claudeCodePluginsPage.skillDetail.skillDetailsSubtitle")}
</p>
<table style={{ width: "100%", borderCollapse: "collapse", fontSize: 14 }}>
<thead>
<tr style={{ borderBottom: "1px solid #dadce0" }}>
<th style={{ textAlign: "left", padding: "12px 0", color: "#5f6368", fontWeight: 500, width: 160 }}>
Property
{t("claudeCodePluginsPage.skillDetail.tableColProperty")}
</th>
<th style={{ textAlign: "left", padding: "12px 0", color: "#5f6368", fontWeight: 500 }}>
{skill.name}
@ -127,7 +146,9 @@ const SkillDetail: React.FC<SkillDetailProps> = ({ skill, onBack }) => {
{/* Right sidebar */}
<div style={{ width: 240, flexShrink: 0 }}>
<div style={{ marginBottom: 24 }}>
<div style={{ fontSize: 12, color: "#5f6368", marginBottom: 4 }}>Status</div>
<div style={{ fontSize: 12, color: "#5f6368", marginBottom: 4 }}>
{t("claudeCodePluginsPage.skillDetail.sidebarStatus")}
</div>
<span
style={{
fontSize: 12,
@ -138,13 +159,17 @@ const SkillDetail: React.FC<SkillDetailProps> = ({ skill, onBack }) => {
fontWeight: 500,
}}
>
{skill.enabled ? "Public" : "Draft"}
{skill.enabled
? t("claudeCodePluginsPage.skillDetail.statusPublic")
: t("claudeCodePluginsPage.skillDetail.statusDraft")}
</span>
</div>
{sourceUrl && (
<div style={{ marginBottom: 24 }}>
<div style={{ fontSize: 12, color: "#5f6368", marginBottom: 4 }}>Source</div>
<div style={{ fontSize: 12, color: "#5f6368", marginBottom: 4 }}>
{t("claudeCodePluginsPage.skillDetail.sidebarSource")}
</div>
<a
href={sourceUrl}
target="_blank"
@ -166,7 +191,9 @@ const SkillDetail: React.FC<SkillDetailProps> = ({ skill, onBack }) => {
{skill.keywords && skill.keywords.length > 0 && (
<div style={{ marginBottom: 24 }}>
<div style={{ fontSize: 12, color: "#5f6368", marginBottom: 8 }}>Tags</div>
<div style={{ fontSize: 12, color: "#5f6368", marginBottom: 8 }}>
{t("claudeCodePluginsPage.skillDetail.sidebarTags")}
</div>
<div style={{ display: "flex", flexWrap: "wrap", gap: 6 }}>
{skill.keywords.map((kw) => (
<span
@ -188,7 +215,9 @@ const SkillDetail: React.FC<SkillDetailProps> = ({ skill, onBack }) => {
)}
<div>
<div style={{ fontSize: 12, color: "#5f6368", marginBottom: 4 }}>Skill ID</div>
<div style={{ fontSize: 12, color: "#5f6368", marginBottom: 4 }}>
{t("claudeCodePluginsPage.skillDetail.sidebarSkillId")}
</div>
<div style={{ fontSize: 12, fontFamily: "monospace", color: "#3c4043", wordBreak: "break-all" }}>
{skill.id}
</div>
@ -200,9 +229,11 @@ const SkillDetail: React.FC<SkillDetailProps> = ({ skill, onBack }) => {
{/* How to Use tab */}
{activeTab === "usage" && (
<div style={{ maxWidth: 640 }}>
<h2 style={{ fontSize: 18, fontWeight: 400, color: "#202124", margin: "0 0 8px 0" }}>Using this skill</h2>
<h2 style={{ fontSize: 18, fontWeight: 400, color: "#202124", margin: "0 0 8px 0" }}>
{t("claudeCodePluginsPage.skillDetail.usingThisSkill")}
</h2>
<p style={{ fontSize: 14, color: "#5f6368", margin: "0 0 24px 0", lineHeight: 1.6 }}>
Once your proxy is set as a marketplace, enable this skill in Claude Code with one command:
{t("claudeCodePluginsPage.skillDetail.usageInstruction")}
</p>
{/* Install command */}
@ -224,7 +255,9 @@ const SkillDetail: React.FC<SkillDetailProps> = ({ skill, onBack }) => {
borderBottom: "1px solid #dadce0",
}}
>
<span style={{ fontSize: 13, color: "#3c4043", fontWeight: 500 }}>Run in Claude Code</span>
<span style={{ fontSize: 13, color: "#3c4043", fontWeight: 500 }}>
{t("claudeCodePluginsPage.skillDetail.runInClaudeCode")}
</span>
<button
onClick={() => copyToClipboard(installCommand, "install")}
style={{
@ -240,7 +273,7 @@ const SkillDetail: React.FC<SkillDetailProps> = ({ skill, onBack }) => {
}}
>
{copiedKey === "install" ? <CheckOutlined /> : <CopyOutlined />}
{copiedKey === "install" ? "Copied" : "Copy"}
{copiedKey === "install" ? t("common.copied") : t("common.copy")}
</button>
</div>
<pre
@ -258,9 +291,9 @@ const SkillDetail: React.FC<SkillDetailProps> = ({ skill, onBack }) => {
</div>
<p style={{ fontSize: 13, color: "#5f6368", lineHeight: 1.6, margin: 0 }}>
Don&apos;t have the marketplace configured yet?{" "}
{t("claudeCodePluginsPage.skillDetail.noMarketplace")}{" "}
<span onClick={() => setActiveTab("setup")} style={{ color: "#1a73e8", cursor: "pointer" }}>
See one-time setup →
{t("claudeCodePluginsPage.skillDetail.seeSetup")}
</span>
</p>
</div>
@ -270,14 +303,17 @@ const SkillDetail: React.FC<SkillDetailProps> = ({ skill, onBack }) => {
{activeTab === "setup" && (
<div style={{ maxWidth: 640 }}>
<h2 style={{ fontSize: 18, fontWeight: 400, color: "#202124", margin: "0 0 8px 0" }}>
One-time marketplace setup
{t("claudeCodePluginsPage.skillDetail.setupHeading")}
</h2>
<p style={{ fontSize: 14, color: "#5f6368", margin: "0 0 24px 0", lineHeight: 1.6 }}>
Add this to{" "}
<code style={{ fontSize: 13, backgroundColor: "#f1f3f4", padding: "1px 6px", borderRadius: 4 }}>
~/.claude/settings.json
</code>{" "}
to point Claude Code at your proxy:
<Trans
i18nKey="claudeCodePluginsPage.skillDetail.setupInstruction"
components={{
code: (
<code style={{ fontSize: 13, backgroundColor: "#f1f3f4", padding: "1px 6px", borderRadius: 4 }} />
),
}}
/>
</p>
<div
style={{
@ -326,7 +362,7 @@ const SkillDetail: React.FC<SkillDetailProps> = ({ skill, onBack }) => {
}}
>
{copiedKey === "settings" ? <CheckOutlined /> : <CopyOutlined />}
{copiedKey === "settings" ? "Copied" : "Copy"}
{copiedKey === "settings" ? t("common.copied") : t("common.copy")}
</button>
</div>
<pre

View file

@ -1138,6 +1138,113 @@
"deleteSuccess": "Skill \"{{name}}\" deleted successfully",
"deleteFailed": "Failed to delete skill"
},
"claudeCodePluginsPage": {
"makeSkillPublicForm": {
"selectAtLeastOne": "Please select at least one skill",
"hubUpdated_one": "Skill Hub updated — {{count}} skill published",
"hubUpdated_other": "Skill Hub updated — {{count}} skills published",
"updateFailed": "Failed to update skills. Please try again.",
"selectSkillsTitle": "Select Skills to Publish",
"selectAll": "Select All ({{count}})",
"visibilityNote": "Selected skills will be visible to all users in the Skill Hub. Deselected skills will be unpublished.",
"noSkillsRegistered": "No skills registered yet.",
"publicBadge": "Public",
"willBePublished_one": "<strong>{{count}}</strong> skill will be published",
"willBePublished_other": "<strong>{{count}}</strong> skills will be published",
"confirmTitle": "Confirm Publish to Skill Hub",
"confirmNote": "<strong>Note:</strong> Published skills will be visible to all users in the Skill Hub tab. Skills not in the list below will be unpublished.",
"skillsToBePublished": "Skills to be published:",
"totalWillBePublished_one": "Total: <strong>{{count}}</strong> skill will be published",
"totalWillBePublished_other": "Total: <strong>{{count}}</strong> skills will be published",
"modalTitle": "Publish to Skill Hub",
"stepSelectSkills": "Select Skills",
"publishToHub": "Publish to Hub"
},
"addPluginForm": {
"noAccessToken": "No access token available",
"enterValidGithubUrl": "Please enter a valid GitHub URL",
"invalidSkillName": "Skill name must be kebab-case (lowercase letters, numbers, and hyphens only)",
"invalidVersion": "Version must be in semantic versioning format (e.g., 1.0.0)",
"invalidEmail": "Invalid email format",
"invalidHomepageUrl": "Invalid homepage URL format",
"registerSuccess": "Skill registered successfully",
"registerFailed": "Failed to register skill",
"modalTitle": "Add New Skill",
"githubUrlLabel": "GitHub URL",
"githubUrlRequired": "Please enter a GitHub URL",
"githubUrlTooltip": "Paste a GitHub URL — repo, folder, or file link. E.g. github.com/org/repo or github.com/org/repo/tree/main/my-skill",
"detected": "Detected: {{label}}",
"skillNameLabel": "Skill Name",
"skillNameRequired": "Please enter skill name",
"skillNamePattern": "Name must be kebab-case (lowercase, numbers, hyphens only)",
"skillNameTooltip": "Unique identifier in kebab-case format (e.g., my-skill)",
"domainLabel": "Domain (Optional)",
"domainTooltip": "Top-level grouping in the Skill Hub (e.g., Productivity)",
"domainPlaceholder": "Productivity",
"namespaceLabel": "Namespace (Optional)",
"namespaceTooltip": "Sub-grouping within domain (e.g., workflows)",
"namespacePlaceholder": "workflows",
"descriptionLabel": "Description (Optional)",
"descriptionTooltip": "Brief description of what the skill does",
"descriptionPlaceholder": "A skill that helps with...",
"categoryLabel": "Category (Optional)",
"categoryTooltip": "Select a category or enter a custom one",
"categoryPlaceholder": "Select or type a category",
"keywordsLabel": "Keywords (Optional)",
"keywordsTooltip": "Comma-separated list of keywords for search",
"keywordsPlaceholder": "search, web, api",
"versionLabel": "Version (Optional)",
"versionTooltip": "Semantic version (e.g., 1.0.0)",
"authorNameLabel": "Author Name (Optional)",
"authorNameTooltip": "Name of the skill author or organization",
"authorNamePlaceholder": "Your Name or Organization",
"authorEmailLabel": "Author Email (Optional)",
"authorEmailRule": "Please enter a valid email",
"authorEmailTooltip": "Contact email for the skill author",
"adding": "Adding...",
"addSkill": "Add Skill"
},
"pluginTable": {
"copiedToClipboard": "Copied to clipboard!",
"colSkillName": "Skill Name",
"copyPluginId": "Copy Plugin ID",
"colVersion": "Version",
"versionNA": "N/A",
"noDescription": "No description",
"colCategory": "Category",
"uncategorized": "Uncategorized",
"colPublic": "Public",
"deleteSkill": "Delete skill",
"noSkillsFound": "No skills found. Add one to get started."
},
"skillDetail": {
"backLink": "Skills",
"tabOverview": "Overview",
"tabUsage": "How to Use",
"propCategory": "Category",
"propDomain": "Domain",
"propNamespace": "Namespace",
"propVersion": "Version",
"propAuthor": "Author",
"propAdded": "Added",
"skillDetailsHeading": "Skill Details",
"skillDetailsSubtitle": "Metadata registered with this skill",
"tableColProperty": "Property",
"sidebarStatus": "Status",
"statusPublic": "Public",
"statusDraft": "Draft",
"sidebarSource": "Source",
"sidebarTags": "Tags",
"sidebarSkillId": "Skill ID",
"usingThisSkill": "Using this skill",
"usageInstruction": "Once your proxy is set as a marketplace, enable this skill in Claude Code with one command:",
"runInClaudeCode": "Run in Claude Code",
"noMarketplace": "Don't have the marketplace configured yet?",
"seeSetup": "See one-time setup →",
"setupHeading": "One-time marketplace setup",
"setupInstruction": "Add this to <code>~/.claude/settings.json</code> to point Claude Code at your proxy:"
}
},
"cloudzeroExportModal": {
"exportDataTitle": "Export Data",
"exportDestination": "Export Destination",

View file

@ -1138,6 +1138,113 @@
"deleteSuccess": "技能「{{name}}」已成功删除",
"deleteFailed": "删除技能失败"
},
"claudeCodePluginsPage": {
"makeSkillPublicForm": {
"selectAtLeastOne": "请至少选择一个技能",
"hubUpdated_one": "技能中心已更新 — 已发布 {{count}} 个技能",
"hubUpdated_other": "技能中心已更新 — 已发布 {{count}} 个技能",
"updateFailed": "更新技能失败,请重试。",
"selectSkillsTitle": "选择要发布的技能",
"selectAll": "全选 ({{count}})",
"visibilityNote": "选中的技能将在技能中心向所有用户展示,取消选中的技能将被取消发布。",
"noSkillsRegistered": "暂无已注册的技能。",
"publicBadge": "公开",
"willBePublished_one": "<strong>{{count}}</strong> 个技能将被发布",
"willBePublished_other": "<strong>{{count}}</strong> 个技能将被发布",
"confirmTitle": "确认发布到技能中心",
"confirmNote": "<strong>注意:</strong>已发布的技能将在技能中心标签页向所有用户展示,不在以下列表中的技能将被取消发布。",
"skillsToBePublished": "将要发布的技能:",
"totalWillBePublished_one": "共 <strong>{{count}}</strong> 个技能将被发布",
"totalWillBePublished_other": "共 <strong>{{count}}</strong> 个技能将被发布",
"modalTitle": "发布到技能中心",
"stepSelectSkills": "选择技能",
"publishToHub": "发布到中心"
},
"addPluginForm": {
"noAccessToken": "No access token available",
"enterValidGithubUrl": "请输入有效的 GitHub URL",
"invalidSkillName": "技能名称必须为 kebab-case 格式(仅限小写字母、数字和连字符)",
"invalidVersion": "版本号必须符合语义化版本格式(如 1.0.0)",
"invalidEmail": "邮箱格式无效",
"invalidHomepageUrl": "主页 URL 格式无效",
"registerSuccess": "技能注册成功",
"registerFailed": "技能注册失败",
"modalTitle": "添加新技能",
"githubUrlLabel": "GitHub URL",
"githubUrlRequired": "请输入 GitHub URL",
"githubUrlTooltip": "粘贴 GitHub URL——仓库、文件夹或文件链接,例如 github.com/org/repo 或 github.com/org/repo/tree/main/my-skill",
"detected": "已识别:{{label}}",
"skillNameLabel": "技能名称",
"skillNameRequired": "请输入技能名称",
"skillNamePattern": "名称必须为 kebab-case 格式(仅限小写字母、数字和连字符)",
"skillNameTooltip": "kebab-case 格式的唯一标识符(如 my-skill)",
"domainLabel": "域(可选)",
"domainTooltip": "技能中心的顶级分组(如 Productivity)",
"domainPlaceholder": "Productivity",
"namespaceLabel": "命名空间(可选)",
"namespaceTooltip": "域内的子分组(如 workflows)",
"namespacePlaceholder": "workflows",
"descriptionLabel": "描述(可选)",
"descriptionTooltip": "技能功能的简要说明",
"descriptionPlaceholder": "一个可以帮助……的技能",
"categoryLabel": "分类(可选)",
"categoryTooltip": "选择分类或输入自定义分类",
"categoryPlaceholder": "选择或输入分类",
"keywordsLabel": "关键词(可选)",
"keywordsTooltip": "用于搜索的逗号分隔关键词列表",
"keywordsPlaceholder": "search, web, api",
"versionLabel": "版本(可选)",
"versionTooltip": "语义化版本号(如 1.0.0)",
"authorNameLabel": "作者名称(可选)",
"authorNameTooltip": "技能作者或组织的名称",
"authorNamePlaceholder": "您的姓名或组织",
"authorEmailLabel": "作者邮箱(可选)",
"authorEmailRule": "请输入有效的邮箱地址",
"authorEmailTooltip": "技能作者的联系邮箱",
"adding": "添加中...",
"addSkill": "添加技能"
},
"pluginTable": {
"copiedToClipboard": "已复制到剪贴板!",
"colSkillName": "技能名称",
"copyPluginId": "复制插件 ID",
"colVersion": "版本",
"versionNA": "N/A",
"noDescription": "暂无描述",
"colCategory": "分类",
"uncategorized": "未分类",
"colPublic": "是否公开",
"deleteSkill": "删除技能",
"noSkillsFound": "未找到技能,点击添加以开始使用。"
},
"skillDetail": {
"backLink": "技能",
"tabOverview": "概览",
"tabUsage": "使用方法",
"propCategory": "分类",
"propDomain": "域",
"propNamespace": "命名空间",
"propVersion": "版本",
"propAuthor": "作者",
"propAdded": "添加时间",
"skillDetailsHeading": "技能详情",
"skillDetailsSubtitle": "该技能注册的元数据",
"tableColProperty": "属性",
"sidebarStatus": "状态",
"statusPublic": "公开",
"statusDraft": "草稿",
"sidebarSource": "来源",
"sidebarTags": "标签",
"sidebarSkillId": "技能 ID",
"usingThisSkill": "使用此技能",
"usageInstruction": "将代理设置为 marketplace 后,通过以下命令在 Claude Code 中启用此技能:",
"runInClaudeCode": "在 Claude Code 中运行",
"noMarketplace": "还未配置 marketplace?",
"seeSetup": "查看一次性配置 →",
"setupHeading": "一次性 marketplace 配置",
"setupInstruction": "将以下内容添加到 <code>~/.claude/settings.json</code>,将 Claude Code 指向您的代理:"
}
},
"cloudzeroExportModal": {
"exportDataTitle": "导出数据",
"exportDestination": "导出目标",