feat: add i18n API, render hooks, and new intercept hooks to plugin system

This commit is contained in:
Linus Rath
2026-04-12 13:57:20 +02:00
parent 44e0e17203
commit 24949e183f
5 changed files with 285 additions and 4 deletions
+25 -1
View File
@@ -1,15 +1,18 @@
// Plugin Loader — loads and activates plugins via blob URL dynamic import
// Plugin Loader loads and activates plugins via blob URL dynamic import
import type { InstalledPlugin, Disposable } from './plugin-types';
import { pluginStorage } from './plugin-storage';
import { createPluginAPI, type PluginAPI } from './plugin-api';
import { removeAllPluginHooks, pluginErrorTracker } from './plugin-hooks';
import { setPluginI18nLocale, clearPluginI18nTranslations } from './plugin-i18n';
import React from 'react';
import ReactDOM from 'react-dom';
import * as ReactJSX from 'react/jsx-runtime';
// --- Shared React (window.__PLUGIN_EXTERNALS__) -------------
let localeSyncInitialised = false;
export function exposePluginExternals(): void {
if (typeof window === 'undefined') return;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -18,6 +21,16 @@ export function exposePluginExternals(): void {
ReactDOM,
ReactJSX,
};
// Sync plugin i18n with the app locale (runs once per page load)
if (!localeSyncInitialised) {
localeSyncInitialised = true;
// Dynamic import avoids a circular dependency chain at module evaluation time
import('@/stores/locale-store').then(({ useLocaleStore }) => {
setPluginI18nLocale(useLocaleStore.getState().locale);
useLocaleStore.subscribe((state) => setPluginI18nLocale(state.locale));
}).catch(() => {/* locale sync is best-effort */});
}
}
// --- Active plugin tracking ----------------------------------
@@ -78,6 +91,14 @@ export async function loadPlugin(plugin: InstalledPlugin): Promise<void> {
// 4. Build sandboxed API
const api = createPluginAPI(plugin);
// 4b. Auto-register translations bundled in the manifest (plugin.locales)
// Plugins may still call api.i18n.addTranslations() in activate() to add more.
if (plugin.locales) {
for (const [locale, strings] of Object.entries(plugin.locales)) {
api.i18n.addTranslations(locale, strings);
}
}
// 5. Call activate
const disposable = await mod.activate(api);
@@ -119,6 +140,9 @@ export function deactivatePlugin(pluginId: string): void {
// Remove all hook subscriptions for this plugin
removeAllPluginHooks(pluginId);
// Clear cached translations (avoids memory leak on repeated enable/disable cycles)
clearPluginI18nTranslations(pluginId);
// Reset error tracker
pluginErrorTracker.reset(pluginId);