agalwood/Motrix · error · PluginManifestInvalid
PLUGIN_MANIFEST_INVALID
PLUGIN_MANIFEST_INVALID
Error message
manifest field "${value}" mixes literal and %key% — whole-field placeholders only What it means
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.
Source
Thrown at src/core/plugin/manifest/i18n-resolve.ts:43
Object.assign(out, flattenLocaleDict(v as Record<string, unknown>, key))
}
}
return out
}
export interface ResolveOptions {
currentDict: ManifestLocaleDict
fallbackDict: ManifestLocaleDict // en-US — required
}
const PLACEHOLDER_RE = /^%([\w.-]+)%$/
const PARTIAL_PLACEHOLDER_RE = /%[\w.-]+%/
function resolveOne(value: string, opts: ResolveOptions): string {
const m = PLACEHOLDER_RE.exec(value)
if (!m) {
if (PARTIAL_PLACEHOLDER_RE.test(value)) {
throw new PluginManifestInvalid(
'plugin.manifest.i18n.mixed_placeholder',
`manifest field "${value}" mixes literal and %key% — whole-field placeholders only`
)
}
return value
}
const key = m[1]
return opts.currentDict[key] ?? opts.fallbackDict[key] ?? value
}
function walkSchemaNode(
node: JsonSchemaNode,
opts: ResolveOptions
): JsonSchemaNode {
const out: JsonSchemaNode = { ...node }
if (typeof node.title === 'string') out.title = resolveOne(node.title, opts)
if (typeof node.description === 'string') {
out.description = resolveOne(node.description, opts)View on GitHub (pinned to 1a708ee577)
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%'.
Example fix
// before — manifest.json
// { "name": "Audio %level% Kit" }
// after — whole-field placeholder pointing at a locale key
// { "name": "%plugin.name%" }
// locales/en-US.json: { "plugin.name": "Audio Pro Kit" } Defensive patterns
Strategy: validation
Validate before calling
const WHOLE = /^%([\w.-]+)%$/
const PARTIAL = /%[\w.-]+%/
function assertNoMixedPlaceholder(field:string){
if(!WHOLE.test(field) && PARTIAL.test(field))
throw new Error(`field "${field}" mixes literal and %key%`)
}
// run over every resolvable string field in the manifest before packaging Type guard
const isWholeOrLiteral = (v:string): boolean => /^%([\w.-]+)%$/.test(v) || !/%[\w.-]+%/.test(v)
Try / catch
try { resolveManifestI18n(manifest, opts) }
catch(e){ if(e instanceof PluginManifestInvalid && e.validationCode==='plugin.manifest.i18n.mixed_placeholder'){ /* move the whole string into the locale dict */ } else throw e } Prevention
- 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.
When it happens
Trigger: 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%.
Common situations: Plugin author assumed sprintf-style interpolation; copied template strings from another i18n system; wanted 'Welcome, %user%' but the host only supports full-field replacement.
Related errors
- ${label} must be a flat string record
- transient-failure
- invalid-url-scheme
- invalid Chrome extension ID: ${id}
- invalid Firefox extension ID: ${id}
AI-assisted analysis of agalwood/Motrix@1a708ee577 (2026-08-12).
Data as JSON: /api/errors/c43bc4e07812a866.
Report an issue: GitHub.