diff --git a/packages/app/e2e/browser/plugin-settings.spec.ts b/packages/app/e2e/browser/plugin-settings.spec.ts index aa435dadee..4a15063cd0 100644 --- a/packages/app/e2e/browser/plugin-settings.spec.ts +++ b/packages/app/e2e/browser/plugin-settings.spec.ts @@ -22,6 +22,8 @@ import { openTabbedSettings, selectSettingsTab, openTabbedSettingsLink, + expectSettingsTooltip, + expectNamedPluginRow, } from "../support/helpers/plugin-settings"; test("display preferences survive a reload", async ({ page }) => { @@ -57,6 +59,12 @@ test("Back to plugins returns from a plugin settings screen", async ({ page }) = test("plugin settings open from the cog and show a tab per screen", async ({ page }) => { await openPluginsPage(page); + await test.step("plugin rows show the display name", async () => { + await expectNamedPluginRow(page); + }); + await test.step("the cog explains itself", async () => { + await expectSettingsTooltip(page); + }); await test.step("the actions menu keeps only plugin actions", async () => { await expectActionsMenuWithoutSettings(page); }); diff --git a/packages/app/e2e/support/helpers/plugin-settings.ts b/packages/app/e2e/support/helpers/plugin-settings.ts index 921e39cd94..53048359f1 100644 --- a/packages/app/e2e/support/helpers/plugin-settings.ts +++ b/packages/app/e2e/support/helpers/plugin-settings.ts @@ -101,8 +101,15 @@ async function openPluginScreen(page: Page) { await openHostSection(page, getServerId(), "plugins"); await openSettingsFromCog(page); } +/** The cog on a plugin's row. Its label is "Settings"; the row tells plugins apart. */ +function settingsButton(page: Page, pluginId: string) { + return page + .getByTestId(`plugin-row-${pluginId}`) + .filter({ visible: true }) + .getByRole("button", { name: "Settings", exact: true }); +} async function openSettingsFromCog(page: Page) { - await page.getByRole("button", { name: "Settings for settings-example", exact: true }).click(); + await settingsButton(page, "settings-example").click(); await expect(page.getByRole("switch", { name: "Show metadata" })).toBeVisible(); // A plugin with one settings screen shows no tabs, and the header names only the plugin. await expect(page.getByTestId("plugin-settings-tabs")).toHaveCount(0); @@ -122,16 +129,29 @@ export async function expectActionsMenuWithoutSettings(page: Page) { export async function expectNoSettingsCogWithoutScreens(page: Page) { await page.getByRole("switch", { name: "settings-example: Disable" }).click(); await expect(page.getByRole("switch", { name: "settings-example: Enable" })).toBeVisible(); - await expect( - page.getByRole("button", { name: "Settings for settings-example", exact: true }), - ).toHaveCount(0); + await expect(settingsButton(page, "settings-example")).toHaveCount(0); await page.getByRole("switch", { name: "settings-example: Enable" }).click(); + await expect(settingsButton(page, "settings-example")).toBeVisible(); +} +export async function expectSettingsTooltip(page: Page) { + await settingsButton(page, "settings-example").hover(); + await expect(page.getByRole("tooltip")).toHaveText("Settings"); + await page.mouse.move(0, 0); +} +/** A manifest `name` titles the row and its controls; the id stays visible beneath it. */ +export async function expectNamedPluginRow(page: Page) { + const row = page.getByTestId(`plugin-row-${TABBED_PLUGIN_ID}`).filter({ visible: true }); + await expect(row.getByText(TABBED_PLUGIN_NAME, { exact: true })).toBeVisible(); + await expect(row.getByText(TABBED_PLUGIN_ID, { exact: true })).toBeVisible(); await expect( - page.getByRole("button", { name: "Settings for settings-example", exact: true }), + page.getByRole("switch", { name: `${TABBED_PLUGIN_NAME}: Disable`, exact: true }), + ).toBeVisible(); + await expect( + page.getByRole("button", { name: `Actions for ${TABBED_PLUGIN_NAME}`, exact: true }), ).toBeVisible(); } export async function openTabbedSettings(page: Page) { - await page.getByRole("button", { name: `Settings for ${TABBED_PLUGIN_ID}`, exact: true }).click(); + await settingsButton(page, TABBED_PLUGIN_ID).click(); await expectSettingsTab(page, "General"); // The Advanced icon reads the plugin's translations; the General icon throws and is dropped. await expect(page.getByTestId("advanced-tab-icon")).toBeVisible(); diff --git a/packages/app/src/i18n/resources/en.ts b/packages/app/src/i18n/resources/en.ts index 2bdc822788..16ba66d960 100644 --- a/packages/app/src/i18n/resources/en.ts +++ b/packages/app/src/i18n/resources/en.ts @@ -2384,7 +2384,7 @@ export const en = { installing: "Installing…", logs: { action: "Logs", - title: "Logs: {{id}}", + title: "Logs: {{name}}", loading: "Loading logs…", refresh: "Refresh", refreshing: "Refreshing…", @@ -2393,8 +2393,8 @@ export const en = { }, status: { running: "running", disabled: "disabled", failed: "failed" }, actions: { - menu: "Actions for {{id}}", - settings: "Settings for {{id}}", + menu: "Actions for {{name}}", + settings: "Settings", reload: "Reload", reloading: "Reloading…", enable: "Enable", @@ -2405,16 +2405,16 @@ export const en = { removing: "Removing…", }, feedback: { - installed: "Installed {{id}}", - reload: "Reloaded {{id}}", - enable: "Enabled {{id}}", - disable: "Disabled {{id}}", - remove: "Removed {{id}}", - kept: "Kept {{id}}", + installed: "Installed {{name}}", + reload: "Reloaded {{name}}", + enable: "Enabled {{name}}", + disable: "Disabled {{name}}", + remove: "Removed {{name}}", + kept: "Kept {{name}}", globalEnabled: "Plugins enabled", globalDisabled: "Plugins disabled", }, - removeConfirmTitle: "Remove {{id}}?", + removeConfirmTitle: "Remove {{name}}?", removeConfirmMessage: "This removes the configuration. The source directory is not deleted.", states: { offlineTitle: "Plugin host is offline", diff --git a/packages/app/src/i18n/resources/plugin-settings.ts b/packages/app/src/i18n/resources/plugin-settings.ts index 9ca2e796ca..a5d829bff1 100644 --- a/packages/app/src/i18n/resources/plugin-settings.ts +++ b/packages/app/src/i18n/resources/plugin-settings.ts @@ -1,7 +1,7 @@ const shared = { logs: { action: "Logs", - title: "Logs: {{id}}", + title: "Logs: {{name}}", loading: "Loading logs…", refresh: "Refresh", refreshing: "Refreshing…", @@ -33,8 +33,8 @@ export const pluginSettings = { installing: "جارٍ التثبيت…", status: { running: "قيد التشغيل", disabled: "معطّلة", failed: "فشلت" }, actions: { - menu: "إجراءات {{id}}", - settings: "إعدادات {{id}}", + menu: "إجراءات {{name}}", + settings: "الإعدادات", reload: "إعادة تحميل", reloading: "جارٍ إعادة التحميل…", enable: "تفعيل", @@ -45,16 +45,16 @@ export const pluginSettings = { removing: "جارٍ الإزالة…", }, feedback: { - installed: "تم تثبيت {{id}}", - reload: "تمت إعادة تحميل {{id}}", - enable: "تم تفعيل {{id}}", - disable: "تم تعطيل {{id}}", - remove: "تمت إزالة {{id}}", - kept: "تم الإبقاء على {{id}}", + installed: "تم تثبيت {{name}}", + reload: "تمت إعادة تحميل {{name}}", + enable: "تم تفعيل {{name}}", + disable: "تم تعطيل {{name}}", + remove: "تمت إزالة {{name}}", + kept: "تم الإبقاء على {{name}}", globalEnabled: "تم تفعيل الإضافات", globalDisabled: "تم تعطيل الإضافات", }, - removeConfirmTitle: "إزالة {{id}}؟", + removeConfirmTitle: "إزالة {{name}}؟", removeConfirmMessage: "يؤدي هذا إلى إزالة الإعداد فقط. لن يُحذف مجلد المصدر.", states: { offlineTitle: "مضيف الإضافات غير متصل", @@ -89,8 +89,8 @@ export const pluginSettings = { installing: "Instalando…", status: { running: "en ejecución", disabled: "desactivado", failed: "fallido" }, actions: { - menu: "Acciones para {{id}}", - settings: "Configuración de {{id}}", + menu: "Acciones para {{name}}", + settings: "Configuración", reload: "Recargar", reloading: "Recargando…", enable: "Activar", @@ -101,16 +101,16 @@ export const pluginSettings = { removing: "Eliminando…", }, feedback: { - installed: "{{id}} instalado", - reload: "{{id}} recargado", - enable: "{{id}} activado", - disable: "{{id}} desactivado", - remove: "{{id}} eliminado", - kept: "Se conservó {{id}}", + installed: "{{name}} instalado", + reload: "{{name}} recargado", + enable: "{{name}} activado", + disable: "{{name}} desactivado", + remove: "{{name}} eliminado", + kept: "Se conservó {{name}}", globalEnabled: "Plugins activados", globalDisabled: "Plugins desactivados", }, - removeConfirmTitle: "¿Eliminar {{id}}?", + removeConfirmTitle: "¿Eliminar {{name}}?", removeConfirmMessage: "Esto elimina la configuración. El directorio de origen no se borra.", states: { offlineTitle: "El host de plugins está sin conexión", @@ -145,8 +145,8 @@ export const pluginSettings = { installing: "Installation…", status: { running: "actif", disabled: "désactivé", failed: "échec" }, actions: { - menu: "Actions pour {{id}}", - settings: "Paramètres de {{id}}", + menu: "Actions pour {{name}}", + settings: "Paramètres", reload: "Recharger", reloading: "Rechargement…", enable: "Activer", @@ -157,16 +157,16 @@ export const pluginSettings = { removing: "Suppression…", }, feedback: { - installed: "{{id}} installé", - reload: "{{id}} rechargé", - enable: "{{id}} activé", - disable: "{{id}} désactivé", - remove: "{{id}} supprimé", - kept: "{{id}} conservé", + installed: "{{name}} installé", + reload: "{{name}} rechargé", + enable: "{{name}} activé", + disable: "{{name}} désactivé", + remove: "{{name}} supprimé", + kept: "{{name}} conservé", globalEnabled: "Plugins activés", globalDisabled: "Plugins désactivés", }, - removeConfirmTitle: "Supprimer {{id}} ?", + removeConfirmTitle: "Supprimer {{name}} ?", removeConfirmMessage: "La configuration sera supprimée. Le dossier source ne sera pas effacé.", states: { offlineTitle: "L’hôte des plugins est hors ligne", @@ -201,8 +201,8 @@ export const pluginSettings = { installing: "インストール中…", status: { running: "実行中", disabled: "無効", failed: "失敗" }, actions: { - menu: "{{id}} のアクション", - settings: "{{id}} の設定", + menu: "{{name}} のアクション", + settings: "設定", reload: "再読み込み", reloading: "再読み込み中…", enable: "有効化", @@ -213,16 +213,16 @@ export const pluginSettings = { removing: "削除中…", }, feedback: { - installed: "{{id}} をインストールしました", - reload: "{{id}} を再読み込みしました", - enable: "{{id}} を有効にしました", - disable: "{{id}} を無効にしました", - remove: "{{id}} を削除しました", - kept: "{{id}} を保持しました", + installed: "{{name}} をインストールしました", + reload: "{{name}} を再読み込みしました", + enable: "{{name}} を有効にしました", + disable: "{{name}} を無効にしました", + remove: "{{name}} を削除しました", + kept: "{{name}} を保持しました", globalEnabled: "プラグインを有効にしました", globalDisabled: "プラグインを無効にしました", }, - removeConfirmTitle: "{{id}} を削除しますか?", + removeConfirmTitle: "{{name}} を削除しますか?", removeConfirmMessage: "設定のみを削除します。ソースディレクトリは削除されません。", states: { offlineTitle: "プラグインホストはオフラインです", @@ -256,8 +256,8 @@ export const pluginSettings = { installing: "설치 중…", status: { running: "실행 중", disabled: "비활성화", failed: "실패" }, actions: { - menu: "{{id}} 작업", - settings: "{{id}} 설정", + menu: "{{name}} 작업", + settings: "설정", reload: "다시 로드", reloading: "다시 로드 중…", enable: "활성화", @@ -268,16 +268,16 @@ export const pluginSettings = { removing: "제거 중…", }, feedback: { - installed: "{{id}} 설치됨", - reload: "{{id}} 다시 로드됨", - enable: "{{id}} 활성화됨", - disable: "{{id}} 비활성화됨", - remove: "{{id}} 제거됨", - kept: "{{id}} 유지됨", + installed: "{{name}} 설치됨", + reload: "{{name}} 다시 로드됨", + enable: "{{name}} 활성화됨", + disable: "{{name}} 비활성화됨", + remove: "{{name}} 제거됨", + kept: "{{name}} 유지됨", globalEnabled: "플러그인 활성화됨", globalDisabled: "플러그인 비활성화됨", }, - removeConfirmTitle: "{{id}}을(를) 제거할까요?", + removeConfirmTitle: "{{name}}을(를) 제거할까요?", removeConfirmMessage: "설정만 제거합니다. 소스 디렉터리는 삭제되지 않습니다.", states: { offlineTitle: "플러그인 호스트가 오프라인입니다", @@ -312,8 +312,8 @@ export const pluginSettings = { installing: "Instalando…", status: { running: "em execução", disabled: "desativado", failed: "falhou" }, actions: { - menu: "Ações para {{id}}", - settings: "Configurações de {{id}}", + menu: "Ações para {{name}}", + settings: "Configurações", reload: "Recarregar", reloading: "Recarregando…", enable: "Ativar", @@ -324,16 +324,16 @@ export const pluginSettings = { removing: "Removendo…", }, feedback: { - installed: "{{id}} instalado", - reload: "{{id}} recarregado", - enable: "{{id}} ativado", - disable: "{{id}} desativado", - remove: "{{id}} removido", - kept: "{{id}} mantido", + installed: "{{name}} instalado", + reload: "{{name}} recarregado", + enable: "{{name}} ativado", + disable: "{{name}} desativado", + remove: "{{name}} removido", + kept: "{{name}} mantido", globalEnabled: "Plugins ativados", globalDisabled: "Plugins desativados", }, - removeConfirmTitle: "Remover {{id}}?", + removeConfirmTitle: "Remover {{name}}?", removeConfirmMessage: "Isso remove a configuração. O diretório de origem não será excluído.", states: { offlineTitle: "O host de plugins está offline", @@ -368,8 +368,8 @@ export const pluginSettings = { installing: "Установка…", status: { running: "работает", disabled: "отключён", failed: "ошибка" }, actions: { - menu: "Действия для {{id}}", - settings: "Настройки {{id}}", + menu: "Действия для {{name}}", + settings: "Настройки", reload: "Перезагрузить", reloading: "Перезагрузка…", enable: "Включить", @@ -380,16 +380,16 @@ export const pluginSettings = { removing: "Удаление…", }, feedback: { - installed: "{{id}} установлен", - reload: "{{id}} перезагружен", - enable: "{{id}} включён", - disable: "{{id}} отключён", - remove: "{{id}} удалён", - kept: "{{id}} сохранён", + installed: "{{name}} установлен", + reload: "{{name}} перезагружен", + enable: "{{name}} включён", + disable: "{{name}} отключён", + remove: "{{name}} удалён", + kept: "{{name}} сохранён", globalEnabled: "Плагины включены", globalDisabled: "Плагины отключены", }, - removeConfirmTitle: "Удалить {{id}}?", + removeConfirmTitle: "Удалить {{name}}?", removeConfirmMessage: "Конфигурация будет удалена. Исходный каталог останется на месте.", states: { offlineTitle: "Хост плагинов не в сети", @@ -423,8 +423,8 @@ export const pluginSettings = { installing: "正在安装…", status: { running: "运行中", disabled: "已禁用", failed: "失败" }, actions: { - menu: "{{id}} 的操作", - settings: "{{id}} 的设置", + menu: "{{name}} 的操作", + settings: "设置", reload: "重新加载", reloading: "正在重新加载…", enable: "启用", @@ -435,16 +435,16 @@ export const pluginSettings = { removing: "正在移除…", }, feedback: { - installed: "已安装 {{id}}", - reload: "已重新加载 {{id}}", - enable: "已启用 {{id}}", - disable: "已禁用 {{id}}", - remove: "已移除 {{id}}", - kept: "已保留 {{id}}", + installed: "已安装 {{name}}", + reload: "已重新加载 {{name}}", + enable: "已启用 {{name}}", + disable: "已禁用 {{name}}", + remove: "已移除 {{name}}", + kept: "已保留 {{name}}", globalEnabled: "插件已启用", globalDisabled: "插件已禁用", }, - removeConfirmTitle: "移除 {{id}}?", + removeConfirmTitle: "移除 {{name}}?", removeConfirmMessage: "这会移除配置,但不会删除源目录。", states: { offlineTitle: "插件主机离线", diff --git a/packages/app/src/plugins/settings/index.tsx b/packages/app/src/plugins/settings/index.tsx index 46e35ad232..f2e510872a 100644 --- a/packages/app/src/plugins/settings/index.tsx +++ b/packages/app/src/plugins/settings/index.tsx @@ -6,11 +6,12 @@ import { router } from "expo-router"; import { ArrowLeft, Settings } from "lucide-react-native"; import type { PluginHostProps } from "@getpaseo/plugin/client"; import { Button } from "@/components/ui/button"; +import { ToolbarButton, paneContentToolbarIconSize } from "@/components/ui/pane-content-toolbar"; import { SegmentedControl, type SegmentedControlOption } from "@/components/ui/segmented-control"; import { useIsCompactFormFactor } from "@/constants/layout"; import { useHostFeature } from "@/runtime/host-features"; import { useHostRuntimeClient, useHostRuntimeIsConnected, useHosts } from "@/runtime/host-runtime"; -import { ICON_SIZE, type Theme } from "@/styles/theme"; +import type { Theme } from "@/styles/theme"; import { PluginIconView } from "../icons"; import { useInstalledPlugin } from "../registry"; import type { InstalledPlugin } from "../types"; @@ -27,7 +28,6 @@ interface SettingsIdentity { const ThemedSettingsIcon = withUnistyles(Settings); const mutedIconMapping = (theme: Theme) => ({ color: theme.colors.foregroundMuted }); -const settingsIcon = ; /** Opens the plugin's settings at its first screen. Renders nothing when it has none. */ export function PluginSettingsButton({ serverId, pluginId }: Omit) { @@ -38,15 +38,17 @@ export function PluginSettingsButton({ serverId, pluginId }: Omit { if (firstScreenId) router.push(buildPluginSettingsRoute(serverId, pluginId, firstScreenId)); }, [serverId, pluginId, firstScreenId]); + const compact = useIsCompactFormFactor(); if (!supported || !firstScreenId) return null; return ( - ), }), - [logs.isFetching, pluginId, refresh, t], + [logs.isFetching, plugin, refresh, t], ); let content = {t("settings.plugins.logs.loading")}; @@ -289,7 +294,7 @@ export function HostPluginsPage({ serverId }: { serverId: string }) { ), [t], ); - const [logsPluginId, setLogsPluginId] = useState(null); + const [logsPlugin, setLogsPlugin] = useState(null); const [feedback, setFeedback] = useState<{ kind: "success" | "error"; message: string } | null>( null, ); @@ -343,27 +348,28 @@ export function HostPluginsPage({ serverId }: { serverId: string }) { run: async () => { const installed = await client.installPluginSource(input); installForm.reset(); - return t("settings.plugins.feedback.installed", { id: installed.id }); + return t("settings.plugins.feedback.installed", { name: pluginDisplayName(installed) }); }, }); }, [client, installForm, installState.canSubmit, mutation, sourceInstallSupported, t]); const action = useCallback( (name: PluginRowAction, plugin: PluginListItem) => { if (!client) return; + const pluginName = pluginDisplayName(plugin); const run = async () => { if (name === "remove") { const confirmed = await confirmDialog({ - title: t("settings.plugins.removeConfirmTitle", { id: plugin.id }), + title: t("settings.plugins.removeConfirmTitle", { name: pluginName }), message: t("settings.plugins.removeConfirmMessage"), confirmLabel: t("settings.plugins.actions.remove"), destructive: true, }); - if (!confirmed) return t("settings.plugins.feedback.kept", { id: plugin.id }); + if (!confirmed) return t("settings.plugins.feedback.kept", { name: pluginName }); await client.removePlugin(plugin.id); } else { await client[`${name}Plugin`](plugin.id); } - return t(`settings.plugins.feedback.${name}`, { id: plugin.id }); + return t(`settings.plugins.feedback.${name}`, { name: pluginName }); }; setFeedback(null); mutation.mutate({ action: name, pluginId: plugin.id, run }); @@ -395,7 +401,7 @@ export function HostPluginsPage({ serverId }: { serverId: string }) { const retry = useCallback(() => { void refetchPlugins(); }, [refetchPlugins]); - const closeLogs = useCallback(() => setLogsPluginId(null), []); + const closeLogs = useCallback(() => setLogsPlugin(null), []); if (pageState === "offline") { return ( @@ -441,7 +447,7 @@ export function HostPluginsPage({ serverId }: { serverId: string }) { pending={mutation.isPending} pendingAction={pending ? pluginRowAction(mutation.variables?.action) : undefined} onAction={action} - onOpenLogs={setLogsPluginId} + onOpenLogs={setLogsPlugin} supportsLogs={logsSupported} /> ); @@ -515,10 +521,10 @@ export function HostPluginsPage({ serverId }: { serverId: string }) { ) : null} {catalogContent} - {client && logsPluginId ? ( + {client && logsPlugin ? ( diff --git a/public-docs/plugins/reference.md b/public-docs/plugins/reference.md index a4bcfebde7..90336f7a7f 100644 --- a/public-docs/plugins/reference.md +++ b/public-docs/plugins/reference.md @@ -73,16 +73,16 @@ The required root manifest is `paseo-plugin.json`: } ``` -| Field | Required | Behavior | -| -------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------- | -| `id` | Yes | Default installation ID. | -| `name` | No | Non-empty display name for the registry, the website, and your plugin settings header; independent of the installation ID. | -| `icon` | No | Relative path to a PNG inside the plugin package. | -| `media` | No | Array of image or video paths inside the package, or HTTPS URLs. An empty array is valid. | -| `description` | No | Non-empty summary shown below the plugin ID in **Settings → Plugins**. | -| `translations` | No | Listing text in other languages, read by the registry and ignored by Paseo. See [other languages](/docs/plugins/publishing#other-languages). | -| `requirements` | No | Supported Paseo versions, described below. | -| `build` | No | Preparation commands, described in the CLI reference. | +| Field | Required | Behavior | +| -------------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | +| `id` | Yes | Default installation ID. | +| `name` | No | Non-empty display name for the registry, the website, your row in **Settings → Plugins**, and your settings header. Without it, Paseo shows the ID. | +| `icon` | No | Relative path to a PNG inside the plugin package. | +| `media` | No | Array of image or video paths inside the package, or HTTPS URLs. An empty array is valid. | +| `description` | No | Non-empty summary shown below the plugin name in **Settings → Plugins**. | +| `translations` | No | Listing text in other languages, read by the registry and ignored by Paseo. See [other languages](/docs/plugins/publishing#other-languages). | +| `requirements` | No | Supported Paseo versions, described below. | +| `build` | No | Preparation commands, described in the CLI reference. | Paths are relative to `paseo-plugin.json`, use forward slashes, and cannot contain `..` segments. Registry listings for visual plugins require an interface image even though `media` is diff --git a/public-docs/plugins/reference.zh-CN.md b/public-docs/plugins/reference.zh-CN.md index 54b6332fbe..a273a70aa7 100644 --- a/public-docs/plugins/reference.zh-CN.md +++ b/public-docs/plugins/reference.zh-CN.md @@ -73,10 +73,10 @@ my-plugin/ | 字段 | 是否必填 | 行为 | | -------------- | -------- | ---------------------------------------------------------------------------------------------------------- | | `id` | 是 | 默认的安装 ID。 | -| `name` | 否 | 注册表、网站和插件设置页标题使用的非空显示名称;与安装 ID 无关。 | +| `name` | 否 | 注册表、网站、**设置 → 插件** 中的插件行以及插件设置页标题使用的非空显示名称。没有 `name` 时显示插件 ID。 | | `icon` | 否 | 插件包内 PNG 文件的相对路径。 | | `media` | 否 | 包内图片或视频路径的数组,或 HTTPS URL。空数组是有效的。 | -| `description` | 否 | 在 **设置 → 插件** 中显示在插件 ID 下方的非空摘要。 | +| `description` | 否 | 在 **设置 → 插件** 中显示在插件名称下方的非空摘要。 | | `translations` | 否 | 其他语言的展示文本,由注册表读取,Paseo 会忽略它。见[其他语言](/docs/plugins/publishing#other-languages)。 | | `requirements` | 否 | 支持的 Paseo 版本,见下文。 | | `build` | 否 | 准备命令,见 CLI 参考。 |