{"record":{"id":"c43bc4e07812a866","repo":"agalwood/Motrix","slug":"plugin-manifest-invalid-c43bc4","errorCode":"PLUGIN_MANIFEST_INVALID","errorMessage":"manifest field \"${value}\" mixes literal and %key% — whole-field placeholders only","messagePattern":"manifest field \"(.+?)\" mixes literal and %key% — whole-field placeholders only","errorType":"validation","errorClass":"PluginManifestInvalid","httpStatus":null,"severity":"error","filePath":"src/core/plugin/manifest/i18n-resolve.ts","lineNumber":43,"sourceCode":"      Object.assign(out, flattenLocaleDict(v as Record<string, unknown>, key))\n    }\n  }\n  return out\n}\n\nexport interface ResolveOptions {\n  currentDict: ManifestLocaleDict\n  fallbackDict: ManifestLocaleDict // en-US — required\n}\n\nconst PLACEHOLDER_RE = /^%([\\w.-]+)%$/\nconst PARTIAL_PLACEHOLDER_RE = /%[\\w.-]+%/\n\nfunction resolveOne(value: string, opts: ResolveOptions): string {\n  const m = PLACEHOLDER_RE.exec(value)\n  if (!m) {\n    if (PARTIAL_PLACEHOLDER_RE.test(value)) {\n      throw new PluginManifestInvalid(\n        'plugin.manifest.i18n.mixed_placeholder',\n        `manifest field \"${value}\" mixes literal and %key% — whole-field placeholders only`\n      )\n    }\n    return value\n  }\n  const key = m[1]\n  return opts.currentDict[key] ?? opts.fallbackDict[key] ?? value\n}\n\nfunction walkSchemaNode(\n  node: JsonSchemaNode,\n  opts: ResolveOptions\n): JsonSchemaNode {\n  const out: JsonSchemaNode = { ...node }\n  if (typeof node.title === 'string') out.title = resolveOne(node.title, opts)\n  if (typeof node.description === 'string') {\n    out.description = resolveOne(node.description, opts)","sourceCodeStart":25,"sourceCodeEnd":61,"githubUrl":"https://github.com/agalwood/Motrix/blob/1a708ee57746c434e2c67a44bbf0906a976afea4/src/core/plugin/manifest/i18n-resolve.ts#L25-L61","documentation":"resolveOne() enforces whole-field-only placeholders: a manifest string field is either a literal, exactly '%key%' (matched by PLACEHOLDER_RE /^%([\\w.-]+)%$/), or rejected if it contains a partial placeholder (matched by PARTIAL_PLACEHOLDER_RE /%[\\w.-]+%/). Mixing literal text with an inline %key% (e.g. 'Hello %name%') is forbidden because the resolver only swaps whole fields, never substrings.","triggerScenarios":"A manifest string field like contributes.configuration.title = 'Settings for %plugin%' or name = '%vendor% Tool' — the field is not purely '%key%' but contains a %token%.","commonSituations":"Plugin author assumed sprintf-style interpolation; copied template strings from another i18n system; wanted 'Welcome, %user%' but the host only supports full-field replacement.","solutions":["Move the full string into the locale dict and reference it whole: field = '%greeting%' with locale {greeting:'Welcome, User'}.","If the literal must stay, remove the %...% token entirely.","Use only whole-field placeholders: a field value must be exactly '%some.key%'."],"exampleFix":"// before — manifest.json\n// { \"name\": \"Audio %level% Kit\" }\n// after — whole-field placeholder pointing at a locale key\n// { \"name\": \"%plugin.name%\" }\n// locales/en-US.json: { \"plugin.name\": \"Audio Pro Kit\" }","handlingStrategy":"validation","validationCode":"const WHOLE = /^%([\\w.-]+)%$/\nconst PARTIAL = /%[\\w.-]+%/\nfunction assertNoMixedPlaceholder(field:string){\n  if(!WHOLE.test(field) && PARTIAL.test(field))\n    throw new Error(`field \"${field}\" mixes literal and %key%`)\n}\n// run over every resolvable string field in the manifest before packaging","typeGuard":"const isWholeOrLiteral = (v:string): boolean => /^%([\\w.-]+)%$/.test(v) || !/%[\\w.-]+%/.test(v)","tryCatchPattern":"try { resolveManifestI18n(manifest, opts) }\ncatch(e){ if(e instanceof PluginManifestInvalid && e.validationCode==='plugin.manifest.i18n.mixed_placeholder'){ /* move the whole string into the locale dict */ } else throw e }","preventionTips":["Use whole-field placeholders only: field value is exactly '%some.key%'.","Put full strings (including any literal prefix/suffix) in the locale dict.","Lint manifest string fields for stray %...% tokens before packaging."],"tags":["manifest","i18n","placeholder","locale","validation"],"backgroundTag":null,"analyzedSha":"1a708ee57746c434e2c67a44bbf0906a976afea4","analyzedAt":"2026-08-12T16:18:09.346Z","schemaVersion":2},"datasetVersion":"2026-08-12T18:17:37.767Z"}