Files
SRCmail/lib/plugin-loader.ts
dealerwebandLinus Rath 2ba0003e16 Feature: localizable sandboxed plugins (manifest locales + api.i18n.t)
The plugin runtime received the active locale (init payload + 'locale-change')
and plugins could declare a `locales` map, but none of it was usable: the
locales never reached the runtime, and buildPluginApi exposed no i18n. So
plugin code calling pluginApi.i18n.t(...) (as the External Link Warning plugin
does) always got undefined and fell back to English.

Thread plugin locales end to end and surface an i18n API:
- ServerPlugin gains `locales`; the upload route persists manifest.locales
  (alongside configSchema/settingsSchema), and /api/plugins surfaces it to the
  client so it flows registry -> client -> sandbox host-bridge -> runtime.
- runtime sets __PLUGIN_LOCALE__ at init (not only on later 'locale-change')
  and buildPluginApi exposes `i18n.locale` + `i18n.t(key, vars)` resolving
  against the plugin's declared locales (manifest.locales) with English/key
  fallback and {placeholder} interpolation.

Lets any sandboxed plugin localize its strings from its manifest.
2026-05-30 15:56:55 +02:00

75 lines
2.6 KiB
TypeScript

// Plugin loader entrypoint. Delegates to the iframe-based sandbox in
// `lib/plugin-sandbox/`. The legacy blob-URL `import()` path has been
// removed; plugin bundles now run in a null-origin sandbox iframe and
// communicate with the host via postMessage RPC.
import type { InstalledPlugin } from './plugin-types';
import {
loadSandboxedPlugin,
unloadSandboxedPlugin,
activateAllSandboxed,
deactivateAllSandboxed,
setSandboxStoreAccessor,
setupSandboxAutoDisable,
} from './plugin-sandbox/loader';
import { all as allActive, get as getActive } from './plugin-sandbox/registry';
// Re-export so the plugin store can keep the sandbox locale in step via this
// facade, instead of importing lib/plugin-sandbox/loader directly (which would
// also pull the hook buses into consumers' module graphs).
export { setSandboxLocale } from './plugin-sandbox/loader';
/**
* Historically re-published React/ReactDOM on `globalThis` for the blob-import
* loader, and later also bootstrapped plugin locale sync. Both are obsolete:
* the sandbox injects React per-iframe, and locale sync now lives where plugin
* activation is orchestrated (stores/plugin-store -> initializePlugins, via
* setSandboxLocale). Kept as a no-op for the legacy activateAllPlugins()
* wrapper and its test.
*/
export function exposePluginExternals(): void {
/* no-op */
}
// ─── Store accessor (status updates) ──────────────────────────
type StoreAccessor = { setPluginStatus: (id: string, status: InstalledPlugin['status'], error?: string) => void };
export function setPluginStoreAccessor(accessor: StoreAccessor): void {
setSandboxStoreAccessor(accessor);
}
// ─── Lifecycle (sandbox-backed) ───────────────────────────────
export async function loadPlugin(plugin: InstalledPlugin): Promise<void> {
if (getActive(plugin.id)) {
console.warn(`[plugin-loader] "${plugin.id}" is already loaded`);
return;
}
await loadSandboxedPlugin(plugin);
}
export function deactivatePlugin(pluginId: string): void {
unloadSandboxedPlugin(pluginId);
}
export async function activateAllPlugins(plugins: InstalledPlugin[]): Promise<void> {
exposePluginExternals();
await activateAllSandboxed(plugins);
}
export function deactivateAllPlugins(): void {
deactivateAllSandboxed();
}
export function isPluginActive(pluginId: string): boolean {
return getActive(pluginId) !== undefined;
}
export function setupAutoDisable(): void {
setupSandboxAutoDisable();
}
// Re-export for stores/tests that need the active set.
export { allActive as activePlugins };