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.
87 lines
3.3 KiB
TypeScript
87 lines
3.3 KiB
TypeScript
import { NextResponse } from 'next/server';
|
|
import { getPluginRegistry, getThemeRegistry } from '@/lib/admin/plugin-registry';
|
|
import { listDevPlugins } from '@/lib/admin/plugin-dev';
|
|
import { configManager } from '@/lib/admin/config-manager';
|
|
import { logger } from '@/lib/logger';
|
|
|
|
/**
|
|
* GET /api/plugins - Public endpoint for clients to discover server-managed plugins & themes
|
|
*
|
|
* Returns all enabled plugins and themes so the client can sync them to IndexedDB.
|
|
* No admin auth required - this is how regular users receive plugins/themes.
|
|
*/
|
|
export async function GET() {
|
|
try {
|
|
await configManager.ensureLoaded();
|
|
const policy = configManager.getPolicy();
|
|
const policyForceEnabledIds = new Set(policy.forceEnabledPlugins || []);
|
|
|
|
const [pluginRegistry, themeRegistry, devEntries] = await Promise.all([
|
|
getPluginRegistry(),
|
|
getThemeRegistry(),
|
|
listDevPlugins(),
|
|
]);
|
|
|
|
// Dev plugins win on id collision so a developer can shadow an installed
|
|
// plugin without uninstalling it first.
|
|
const devIds = new Set(devEntries.map(e => e.plugin.id));
|
|
const installedEnabled = pluginRegistry.plugins.filter(p => p.enabled && !devIds.has(p.id));
|
|
|
|
const plugins = [
|
|
...devEntries.map(e => ({ ...e.plugin, dev: true })),
|
|
...installedEnabled.map(p => ({ ...p, dev: false })),
|
|
].map(p => ({
|
|
id: p.id,
|
|
name: p.name,
|
|
version: p.version,
|
|
author: p.author,
|
|
description: p.description,
|
|
type: p.type,
|
|
permissions: p.permissions,
|
|
entrypoint: p.entrypoint,
|
|
// Policy is the canonical source for force-enable. The per-plugin field
|
|
// can drift for dev plugins (manifest always loads forceEnabled:false)
|
|
// and during pending policy saves; OR'ing here unifies the signal so
|
|
// the client's auto-enable path triggers consistently.
|
|
forceEnabled: p.forceEnabled || policyForceEnabledIds.has(p.id),
|
|
// Content hash + updatedAt let clients detect re-uploads even when
|
|
// the manifest version is unchanged.
|
|
bundleHash: p.bundleHash,
|
|
updatedAt: p.updatedAt,
|
|
// Marks plugins loaded from PLUGIN_DEV_DIR. Surface in UI as a badge.
|
|
dev: p.dev,
|
|
// Surface so clients can enforce api.http.fetch origin allowlists.
|
|
httpOrigins: p.httpOrigins,
|
|
// Surface so clients can enforce api.http.post path allowlists.
|
|
apiPostPaths: p.apiPostPaths,
|
|
// Per-user settings schema, captured from the manifest at upload/load
|
|
// time so the client can render the settings UI without re-parsing.
|
|
settingsSchema: p.settingsSchema,
|
|
// Plugin-declared i18n tables, so the sandbox can localize plugin
|
|
// strings via api.i18n.t().
|
|
locales: p.locales,
|
|
}));
|
|
|
|
// Only serve enabled themes
|
|
const themes = themeRegistry.themes
|
|
.filter(t => t.enabled)
|
|
.map(t => ({
|
|
id: t.id,
|
|
name: t.name,
|
|
version: t.version,
|
|
author: t.author,
|
|
description: t.description,
|
|
variants: t.variants,
|
|
forceEnabled: t.forceEnabled || false,
|
|
}));
|
|
|
|
return NextResponse.json(
|
|
{ plugins, themes },
|
|
{ headers: { 'Cache-Control': 'no-store' } },
|
|
);
|
|
} catch (error) {
|
|
logger.error('Plugin list error', { error: error instanceof Error ? error.message : 'Unknown error' });
|
|
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
|
|
}
|
|
}
|