Migration (multi-agent sweep over 216 files, 60 batches):
- Every user-visible dashboard + TUI string moved to t() with the exact
English inline default (en rendering byte-identical)
- Catalogs merged from per-batch fragments: en/zh-CN/zh-TW/fr/es now
carry ~5,930 keys each across common/app/errors/cli namespaces;
CLI bundles regenerated (6 locales incl. ko)
Integration fixes:
- 18 type errors: reserved {{count}} interpolations renamed, malformed
plural call, hand-rolled t-param types replaced with TFunction<"app">
- 23 lint errors: superseded label constants/helpers removed
- ExecutorStatusBar hook-order violation (keyboard-open early return
moved below hooks)
- TUI tests wrapped in I18nextProvider (uninitialized fallback renders
literal {{placeholders}}); dashboard vitest.setup boots a minimal en
i18next instance for the same reason
Known WIP (next commits): ~457 residual strings across 50 batches,
Korean drafts for swept keys, and a dashboard test-suite pass that is
still being stabilized (~283 failures under investigation — fake-timer
waitFor interaction, likely stale node_modules vs merged lockfile).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
80 lines
2.7 KiB
TypeScript
80 lines
2.7 KiB
TypeScript
import type { ReactNode } from "react";
|
|
import { useTranslation } from "react-i18next";
|
|
import { ErrorBoundary } from "./ErrorBoundary";
|
|
import { usePluginUiSlots } from "../hooks/usePluginUiSlots";
|
|
import { resolvePluginSlotComponent, type PluginSlotHostActions } from "../plugins/pluginSlotRegistry";
|
|
import "./PluginSlot.css";
|
|
|
|
interface PluginSlotProps {
|
|
/** The slot identifier to render (e.g., "task-detail-tab", "header-action") */
|
|
slotId: string;
|
|
/** Optional project ID for multi-project slot scoping */
|
|
projectId?: string;
|
|
/** Optional plugin IDs to restrict rendering to a subset of matching entries */
|
|
pluginIds?: string[];
|
|
/** Render unresolved entry shell states for unregistered slot components */
|
|
renderPlaceholder?: boolean;
|
|
/** Optional host-controlled callbacks that slot components can call */
|
|
actions?: PluginSlotHostActions;
|
|
}
|
|
|
|
function PluginSlotMissingComponent({ slotId, pluginId }: { slotId: string; pluginId: string }): ReactNode {
|
|
const { t } = useTranslation("app");
|
|
return (
|
|
<section
|
|
className="plugin-slot-shell"
|
|
data-plugin-slot
|
|
data-slot-id={slotId}
|
|
data-plugin-id={pluginId}
|
|
data-plugin-slot-state="missing-component"
|
|
role="status"
|
|
aria-live="polite"
|
|
>
|
|
<p className="plugin-slot-shell__title">{t("plugins.componentUnavailable", "Plugin component unavailable")}</p>
|
|
<p className="plugin-slot-shell__message">
|
|
{t("plugins.couldNotResolve", "The dashboard could not resolve this plugin surface from the static host registry.")}
|
|
</p>
|
|
</section>
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Renders plugin slot registrations for a host surface.
|
|
*/
|
|
export function PluginSlot({ slotId, projectId, pluginIds, renderPlaceholder = true, actions }: PluginSlotProps): ReactNode {
|
|
const { getSlotsForId, loading, error } = usePluginUiSlots(projectId);
|
|
|
|
if (loading || error || !slotId) {
|
|
return null;
|
|
}
|
|
|
|
const matchingEntries = getSlotsForId(slotId).filter((entry) =>
|
|
pluginIds && pluginIds.length > 0 ? pluginIds.includes(entry.pluginId) : true,
|
|
);
|
|
|
|
if (matchingEntries.length === 0) {
|
|
return null;
|
|
}
|
|
|
|
return (
|
|
<ErrorBoundary level="page">
|
|
<>
|
|
{matchingEntries.map((entry, index) => {
|
|
const key = `${entry.pluginId}-${entry.slot.slotId}-${index}`;
|
|
const SlotComponent = resolvePluginSlotComponent(entry);
|
|
|
|
if (SlotComponent) {
|
|
return <SlotComponent key={key} entry={entry} actions={actions} />;
|
|
}
|
|
|
|
if (!renderPlaceholder) {
|
|
return null;
|
|
}
|
|
|
|
return <PluginSlotMissingComponent key={key} slotId={entry.slot.slotId} pluginId={entry.pluginId} />;
|
|
})}
|
|
</>
|
|
</ErrorBoundary>
|
|
);
|
|
}
|