From 7f6a0651e852b51c0df0321f5afa670cdbb9d89d Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Tue, 20 Oct 2026 08:39:50 +0200 Subject: [PATCH] fix(server): leave plugin listing translations to the registry The daemon rejected manifests whose translations the registry would refuse, so a plugin main installs could fail to load here. Paseo never reads translations; the registry validates them at publish. Docs now say which checks are automated at publish and which are human review. --- .../src/server/plugins/manifest.test.ts | 26 +++++++++---------- .../server/src/server/plugins/manifest.ts | 25 ------------------ public-docs/plugins/publishing.md | 21 +++++++++------ public-docs/plugins/publishing.zh-CN.md | 6 ++--- public-docs/plugins/reference.md | 24 +++++++---------- public-docs/plugins/reference.zh-CN.md | 22 +++++++--------- 6 files changed, 48 insertions(+), 76 deletions(-) diff --git a/packages/server/src/server/plugins/manifest.test.ts b/packages/server/src/server/plugins/manifest.test.ts index 416616757d..3cbd4ae19a 100644 --- a/packages/server/src/server/plugins/manifest.test.ts +++ b/packages/server/src/server/plugins/manifest.test.ts @@ -68,11 +68,6 @@ describe("plugin manifest", () => { { id: "INVALID" }, { description: 42 }, { requirements: { paseoo: ">=0.11.0" } }, - { translations: { "zh-cn": { name: "示例" } } }, - { translations: { en: { name: "Example" } } }, - { translations: { "not a tag": { name: "Example" } } }, - { translations: { "zh-CN": { name: " " } } }, - { translations: { "zh-CN": { desc: "示例" } } }, ])("rejects invalid known fields: %j", async (fields) => { const directory = await mkdtemp(path.join(tmpdir(), "paseo-plugin-manifest-")); directories.push(directory); @@ -133,17 +128,20 @@ describe("plugin manifest", () => { await expect(readPluginManifest(directory)).rejects.toThrow(); }); - it("reads listing translations with each field optional", async () => { + // Listing translations are read by the registry when it publishes, never by the daemon, so a + // value the registry would refuse must not stop a plugin from installing. + it.each([ + { "zh-cn": { name: "示例" } }, + { "zh-CN": { desc: "示例" } }, + "zh-CN", + ])("ignores listing translations: %j", async (translations) => { const directory = await mkdtemp(path.join(tmpdir(), "paseo-plugin-manifest-")); directories.push(directory); - const manifest = { - id: "described", - name: "Reviewer", - description: "Reviews changes before merge", - translations: { "zh-CN": { description: "合并前审查改动" } }, - }; - await writeFile(path.join(directory, "paseo-plugin.json"), JSON.stringify(manifest)); - await expect(readPluginManifest(directory)).resolves.toEqual(manifest); + await writeFile( + path.join(directory, "paseo-plugin.json"), + JSON.stringify({ id: "described", translations }), + ); + await expect(readPluginManifest(directory)).resolves.toEqual({ id: "described" }); }); it("accepts only non-empty argv arrays for build commands", async () => { diff --git a/packages/server/src/server/plugins/manifest.ts b/packages/server/src/server/plugins/manifest.ts index 39ddba422d..2312a2cb5a 100644 --- a/packages/server/src/server/plugins/manifest.ts +++ b/packages/server/src/server/plugins/manifest.ts @@ -22,27 +22,10 @@ const PluginAssetPathSchema = z value.split("/").at(-1) !== ".", "Expected a relative file path inside the plugin package", ); -// Keys are canonical language tags such as `zh-CN`, so a miscased `zh-cn` fails instead of never -// matching a reader's language. A misspelled field would silently show English, so entries are strict. -const PluginListingTranslationsSchema = z.record( - z - .string() - .refine( - (tag) => tag !== "en" && isCanonicalLanguageTag(tag), - "Expected a canonical language tag other than en, such as zh-CN", - ), - z - .object({ - name: z.string().trim().min(1).optional(), - description: z.string().trim().min(1).optional(), - }) - .strict(), -); const PluginManifestSchema = z.object({ id: PluginIdSchema, name: z.string().trim().min(1).optional(), description: z.string().trim().min(1).optional(), - translations: PluginListingTranslationsSchema.optional(), icon: PluginAssetPathSchema.refine( (value) => /\.png$/i.test(value), "Expected a relative path to a PNG", @@ -53,14 +36,6 @@ const PluginManifestSchema = z.object({ build: z.array(PluginBuildCommandSchema).min(1).optional(), }); -function isCanonicalLanguageTag(tag: string): boolean { - try { - return Intl.getCanonicalLocales(tag)[0] === tag; - } catch { - return false; - } -} - export type PluginManifest = z.infer; export async function readPluginManifest(directory: string): Promise { diff --git a/public-docs/plugins/publishing.md b/public-docs/plugins/publishing.md index 33aec3df99..101b7c1caf 100644 --- a/public-docs/plugins/publishing.md +++ b/public-docs/plugins/publishing.md @@ -51,8 +51,8 @@ Also supply a one-sentence description: use `package.json`'s `description` for n ### Other languages -The website shows listings in English and Simplified Chinese. English is required; translations are -optional. Add translated listing text to `paseo-plugin.json`, keyed by language tag: +Listings show in English and Simplified Chinese. English is required and translations are optional. +Add a translated `name` and `description` to `paseo-plugin.json`, keyed by language tag: ```json { @@ -65,13 +65,18 @@ optional. Add translated listing text to `paseo-plugin.json`, keyed by language } ``` -Each field is optional. A missing translated `name` or `description` shows the English one. +Each field is optional, and a missing one shows the English text. The registry checks +`translations` when it publishes the plugin: keys are canonical language tags other than `en`, and +each value sets only `name` or `description`, as non-empty strings. Paseo itself does not read +`translations`, so a mistake there fails publication, not installation. -To translate the overview, put a whole copy beside `OVERVIEW.md` named for the language, such as -`OVERVIEW.zh-CN.md`, and include it in the published package. The scaffold's `files` list already -includes `OVERVIEW.*.md`. Readers in that language see the translated overview; without one, they -see the English overview with a notice. Keep the translation in step with `OVERVIEW.md`; the overview rules -above apply to it too. +To translate the overview, publish a complete copy named for the language next to `OVERVIEW.md`, +such as `OVERVIEW.zh-CN.md`. The scaffold's `files` list already includes `OVERVIEW.*.md`. The +registry reads every `OVERVIEW..md` as a translation and fails publication on a tag that is not +a canonical language tag, so do not name other files that way. Readers in that language see the +translation; others see the English overview with a notice. The overview rules above apply to +translations, so update them when `OVERVIEW.md` changes. Reviewers can add or correct translations +in the registry. ## Icons and screenshots diff --git a/public-docs/plugins/publishing.zh-CN.md b/public-docs/plugins/publishing.zh-CN.md index b388df4969..c4406f6ace 100644 --- a/public-docs/plugins/publishing.zh-CN.md +++ b/public-docs/plugins/publishing.zh-CN.md @@ -35,7 +35,7 @@ nav: 发布 ### 其他语言 {#other-languages} -网站以英文和简体中文显示展示页。英文是必需的;翻译是可选的。在 `paseo-plugin.json` 中添加翻译后的展示文本,以语言标签为键: +展示页以英文和简体中文显示。英文是必需的,翻译是可选的。在 `paseo-plugin.json` 中添加翻译后的 `name` 和 `description`,以语言标签为键: ```json { @@ -48,9 +48,9 @@ nav: 发布 } ``` -每个字段都是可选的。缺少翻译后的 `name` 或 `description` 时,会显示英文的值。 +每个字段都是可选的,缺少的字段会显示英文。注册表在发布插件时检查 `translations`:键必须是除 `en` 之外的标准语言标签,每个值只能设置 `name` 或 `description`,且必须是非空字符串。Paseo 本身不读取 `translations`,因此这里的错误会导致发布失败,而不会影响安装。 -要翻译概览,请在 `OVERVIEW.md` 旁边放一份以语言命名的完整副本,例如 `OVERVIEW.zh-CN.md`,并把它包含在发布的包中。脚手架的 `files` 列表已经包含 `OVERVIEW.*.md`。使用该语言的读者会看到翻译后的概览;没有翻译时,他们会看到英文概览和一条提示。请让翻译与 `OVERVIEW.md` 保持同步;上面的概览规则同样适用于它。 +要翻译概览,请在 `OVERVIEW.md` 旁边发布一份以语言命名的完整副本,例如 `OVERVIEW.zh-CN.md`。脚手架的 `files` 列表已经包含 `OVERVIEW.*.md`。注册表会把每个 `OVERVIEW.<标签>.md` 都当作翻译,标签不是标准语言标签时发布会失败,因此不要用这种方式命名其他文件。使用该语言的读者会看到翻译;其他读者会看到英文概览和一条提示。上面的概览规则同样适用于翻译,因此 `OVERVIEW.md` 变更时请同步更新翻译。审查者可以在注册表中补充或修正翻译。 ## 图标和截图 {#icons-and-screenshots} diff --git a/public-docs/plugins/reference.md b/public-docs/plugins/reference.md index a02594135b..f0e9aca07c 100644 --- a/public-docs/plugins/reference.md +++ b/public-docs/plugins/reference.md @@ -52,16 +52,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 and website; 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 `name` and `description` in other languages, keyed by language tag. | -| `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 and website; 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. | 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 @@ -74,10 +74,6 @@ Unknown top-level fields are ignored. Known fields still validate, and unknown k Manifests using `name`, `icon`, or `media` require Paseo 0.11.0 or later; older daemons reject these fields during installation. -`translations` keys are canonical language tags other than `en`, such as `zh-CN`. Each value may -set `name`, `description`, or both, as non-empty strings; other keys are rejected. See -[other languages](/docs/plugins/publishing#other-languages) for how listings use them. - ### Requirements `requirements` is an optional object. Its currently supported key, `paseo`, accepts an npm semver diff --git a/public-docs/plugins/reference.zh-CN.md b/public-docs/plugins/reference.zh-CN.md index 93d54a5c54..5354a942e1 100644 --- a/public-docs/plugins/reference.zh-CN.md +++ b/public-docs/plugins/reference.zh-CN.md @@ -50,23 +50,21 @@ my-plugin/ } ``` -| 字段 | 是否必填 | 行为 | -| -------------- | -------- | -------------------------------------------------------- | -| `id` | 是 | 默认的安装 ID。 | -| `name` | 否 | 注册表和网站使用的非空显示名称;与安装 ID 无关。 | -| `icon` | 否 | 插件包内 PNG 文件的相对路径。 | -| `media` | 否 | 包内图片或视频路径的数组,或 HTTPS URL。空数组是有效的。 | -| `description` | 否 | 在 **设置 → 插件** 中显示在插件 ID 下方的非空摘要。 | -| `translations` | 否 | 其他语言的展示 `name` 和 `description`,以语言标签为键。 | -| `requirements` | 否 | 支持的 Paseo 版本,见下文。 | -| `build` | 否 | 准备命令,见 CLI 参考。 | +| 字段 | 是否必填 | 行为 | +| -------------- | -------- | ---------------------------------------------------------------------------------------------------------- | +| `id` | 是 | 默认的安装 ID。 | +| `name` | 否 | 注册表和网站使用的非空显示名称;与安装 ID 无关。 | +| `icon` | 否 | 插件包内 PNG 文件的相对路径。 | +| `media` | 否 | 包内图片或视频路径的数组,或 HTTPS URL。空数组是有效的。 | +| `description` | 否 | 在 **设置 → 插件** 中显示在插件 ID 下方的非空摘要。 | +| `translations` | 否 | 其他语言的展示文本,由注册表读取,Paseo 会忽略它。见[其他语言](/docs/plugins/publishing#other-languages)。 | +| `requirements` | 否 | 支持的 Paseo 版本,见下文。 | +| `build` | 否 | 准备命令,见 CLI 参考。 | 路径相对于 `paseo-plugin.json`,使用正斜杠,并且不能包含 `..` 片段。即使 `media` 在运行时 manifest 中是可选的,可视化插件的注册表展示页仍然需要一张界面图片。接受的格式和展示页要求见[图标和截图](/docs/plugins/publishing#icons-and-screenshots)。Paseo 会校验这些引用,但不会打开本地资源或获取 URL。 未知的顶层字段会被忽略。已知字段仍然会被校验,`requirements` 中的未知键会被拒绝,以免拼写错误的约束悄悄跳过兼容性检查。使用 `name`、`icon` 或 `media` 的 manifest 需要 Paseo 0.11.0 或更高版本;较旧的 daemon 会在安装时拒绝这些字段。 -`translations` 的键是除 `en` 之外的标准语言标签,例如 `zh-CN`。每个值可以设置 `name`、`description` 或两者,必须是非空字符串;其他键会被拒绝。展示页如何使用它们,见[其他语言](/docs/plugins/publishing#other-languages)。 - ### 版本要求 {#requirements} `requirements` 是一个可选对象。它目前支持的键 `paseo` 接受一个 npm semver 范围。省略 `requirements.paseo` 表示 `<0.8.0`:即该插件早于第一个破坏性的插件版本。Paseo 0.8 及更高版本会拒绝它,并附上[迁移指南](migration)的链接。空字符串、无效的范围以及未知的 manifest 要求键都会被拒绝。