Leantime/leantime · info · RuntimeException
Plugin %s is not installed
Error message
Plugin %s is not installed
What it means
This warning is emitted by the artisan command `translations:check-unused` (app/Command/CheckTranslations.php:134). The command parses app/Language/en-US.ini into a key list, then greps every .php/.blade.php/.tpl.php/.html/.js file under app/ (excluding vendor/node_modules/.git/storage/cache) for literal usages of each key. Any key whose literal string never appears is reported as unused and counted via sprintf('Found %d unused translations:', ...). It is a hygiene report, not a runtime failure — but it is prone to false positives because it only matches literal key text.
Source
Thrown at app/Command/DisablePluginCommand.php:38
{
/**
* {@inheritdoc}
*/
protected function configure(): void
{
$this->addArgument('plugin', InputArgument::REQUIRED, 'The plugin name');
}
/**
* {@inheritdoc}
*/
protected function executeCommand(): int
{
$name = $this->input->getArgument('plugin');
$plugin = $this->getPlugin($name);
if (! isset($plugin->id)) {
throw new RuntimeException(sprintf('Plugin %s is not installed', $plugin->name));
}
if (! $plugin->enabled) {
throw new RuntimeException(sprintf('Plugin %s is not enabled', $plugin->name));
}
if (! $this->confirm(sprintf('Disable plugin %s', $plugin->name))) {
return Command::SUCCESS;
}
return $this->plugins->disablePlugin($plugin->id) ? Command::SUCCESS : Command::FAILURE;
}
}
View on GitHub (pinned to 9a9f49f100)
Solutions
- Re-run with export to review the full list offline: `php bin/leantime translations:check-unused --export=storage/unused-translations.json`.
- Manually verify each reported key is truly dynamic before deleting: `rg -n "<key>" app/ public/ --glob '!vendor'` including JS assets built into public/dist.
- For dynamically-composed keys (e.g. `status_' . $status`), add the base prefix to an allowlist in your review process instead of deleting, or refactor the call sites to use a constant key so the scanner can see it.
- Delete genuinely dead keys from app/Language/en-US.ini (and the mirrored keys in other language INI files) in a dedicated commit.
- If keys are used only by a plugin, move them into the plugin's own language file rather than core en-US.ini.
Example fix
// before — dynamic key, scanner reports 'todo.status_3' as unused
$label = __('todo.status_' . $ticket['status']);
// after — explicit key the scanner (and readers) can see
$statusKeyMap = [1 => 'todo.status_open', 2 => 'todo.status_progress', 3 => 'todo.status_done'];
$label = __($statusKeyMap[$ticket['status']] ?? 'todo.status_unknown'); Defensive patterns
Strategy: validation
Validate before calling
// Before deleting a reported key, validate it is not used dynamically or in built assets
$key = 'todo.status_3';
$usedInCode = \Illuminate\Support\Facades\Process::run(
['grep', '-rq', $key, app_path(), public_path('dist')]
)->successful();
$usedDynamically = \Illuminate\Support\Facades\Process::run(
['grep', '-rq', "'todo.status_'", app_path()]
)->successful(); // dynamic base prefix found -> keep the family of keys
if (! $usedInCode && ! $usedDynamically) { /* safe to remove from en-US.ini */ } Prevention
- Always pass --export and review the JSON report instead of parsing console output.
- Grep public/dist too: keys referenced from compiled JS survive the scanner's app/-only scan.
- Avoid building translation keys by concatenation; use explicit constant keys so static scans stay accurate.
- Keep plugin-specific strings in the plugin's own language files so core 'unused' reports do not mislead.
When it happens
Trigger: Running `php bin/leantime translations:check-unused` when: (1) a translation key is referenced dynamically (key built by string concatenation or `__($type . '.title')`) so the literal never appears in source; (2) the key is only used inside app/Plugins (scanned separately), tests, or excluded directories; (3) a feature was removed and its language keys were never pruned from en-US.ini; (4) the key is used in a file type the Finder does not include (it chains multiple ->name() calls, which the Symfony Finder treats as an OR of patterns per-name, so some patterns may not all apply as expected).
Common situations: Post-refactor cleanup after the .tpl.php → Blade migration (old template keys orphaned); after canvas-variant consolidation into the Blueprints domain left legacy keys behind; after deleting legacy REST API controllers that had their own labels; before a release when trimming the ~thousand-key INI files; export via `--export=unused.json` for bulk review.
Related errors
- Invalid plugin name: %s
- Plugin %s is not installed
- Plugin %s is not enabled
- This endpoint only supports PATCH requests
- sprintf($this->language->__($translationKey), ...$values)
AI-assisted analysis of Leantime/leantime@9a9f49f100 (2026-08-21).
Data as JSON: /api/errors/83b303bdf2ead527.
Report an issue: GitHub.