Files
Termix/src/ui/plugin-host/PluginViewPlaceholder.tsx
T
ZacharyZcRandZacharyZcR 764d034098 fix: show plugin names and descriptions in the UI language (#1658)
* fix: show plugin names and descriptions in the UI language

Read plugin.name and plugin.description from each plugin's own locale
namespace, falling back to the manifest, everywhere core shows a plugin's
name: the Plugins manager, onboarding picker, host editor tabs and
sections, host filters and bulk actions, beta updates, interface presets
and plugin crash/loading notices.

* docs(sdk): document plugin.name and plugin.description locale keys

---------

Co-authored-by: ZacharyZcR <zkl205305@gmail.com>
2026-10-16 23:09:50 -05:00

83 lines
2.7 KiB
TypeScript

import { CircleSlash } from "lucide-react";
import { useTranslation } from "react-i18next";
import { Button } from "@/components/button";
import { usePermissions } from "@/hooks/use-permissions";
import { pluginName } from "@/lib/plugin-i18n";
import { OPEN_PLUGINS_EVENT } from "@/plugins/open-plugins";
import { usePluginStore } from "./plugin-store";
import { unregisteredViewStatus, type ViewKind } from "./view-ownership";
/**
* Stands in for a tab, panel or card whose plugin is not running.
*
* A saved tab of a plugin that was switched off or removed must neither crash
* the shell nor vanish without a word, so it renders this until the plugin is
* back, at which point the real view takes over in place.
*/
export function PluginViewPlaceholder({
kind,
viewId,
compact = false,
}: {
kind: ViewKind;
viewId: string;
compact?: boolean;
}) {
const { t } = useTranslation();
// Subscribing re-renders this when plugin state changes.
usePluginStore();
const { has } = usePermissions();
const { status, owner } = unregisteredViewStatus(kind, viewId);
const name = owner ? pluginName(t, owner.summary) : viewId;
if (status === "loading") {
return (
<div
className="flex h-full w-full items-center justify-center"
aria-label={t("plugins.runtime.loading", { name })}
>
<div className="size-5 rounded-full border-2 border-muted-foreground/30 border-t-muted-foreground/70 animate-spin" />
</div>
);
}
const title =
status === "failed"
? t("plugins.runtime.failed", { name })
: owner
? t("plugins.runtime.needsPlugin", { name })
: t("plugins.runtime.needsUnknownPlugin");
const hint =
status === "failed"
? t("plugins.runtime.failedHint")
: status === "disabled"
? t("plugins.runtime.disabledHint")
: t("plugins.runtime.missingHint");
return (
<div
data-testid="plugin-view-placeholder"
data-status={status}
className={`flex h-full w-full flex-col items-center justify-center gap-2 text-center ${compact ? "p-3" : "p-6"}`}
>
<CircleSlash className="size-6 text-muted-foreground/40" />
<span className="text-sm font-semibold text-muted-foreground">
{title}
</span>
<span className="text-xs text-muted-foreground/70">{hint}</span>
{!compact && status !== "failed" && has("admin.plugins.manage") && (
<Button
variant="outline"
size="sm"
className="mt-1"
onClick={() =>
window.dispatchEvent(new CustomEvent(OPEN_PLUGINS_EVENT))
}
>
{t("plugins.manager.browsePlugins")}
</Button>
)}
</div>
);
}