feat: show plugin setting labels in the user's language when the plugin translates them

This commit is contained in:
Linus Rath
2026-10-14 06:24:32 +02:00
parent 49511dff38
commit 11b1ec2fa5
2 changed files with 49 additions and 3 deletions
@@ -0,0 +1,37 @@
import { describe, it, expect, vi } from 'vitest';
import { fireEvent, render, screen } from '@testing-library/react';
vi.mock('next-intl', () => ({ useTranslations: () => (key: string) => key, useLocale: () => 'de' }));
vi.mock('@/hooks/use-config', () => ({ useConfig: () => ({ demoMode: false }) }));
import { PluginsSettings } from '../plugins-settings';
import { usePluginStore } from '@/stores/plugin-store';
import { usePolicyStore } from '@/stores/policy-store';
import type { InstalledPlugin } from '@/lib/plugin-types';
const PLUGIN = {
id: 'p', name: 'P', version: '1.0.0', author: 'a', description: 'd', type: 'hook', permissions: [], entrypoint: 'index.js',
enabled: true, status: 'running', managed: true, settings: {},
settingsSchema: {
mode: { type: 'select', label: 'settings.mode', description: 'settings.mode_help', default: 'fast', options: ['fast', 'careful'] },
plain: { type: 'boolean', label: 'Plain label', default: false },
},
locales: { de: { 'settings.mode': 'Modus', 'settings.mode_help': 'Wie gearbeitet wird', fast: 'Schnell' } },
} as unknown as InstalledPlugin;
describe('plugin settings labels', () => {
it('shows labels, descriptions and options from the plugin\'s own translations', () => {
usePolicyStore.setState({ loaded: true, isFeatureEnabled: () => true, isPluginForceEnabled: () => false } as never);
usePluginStore.setState({ plugins: [PLUGIN], initialized: true, initializePlugins: vi.fn() } as never);
render(<PluginsSettings />);
fireEvent.click(screen.getByText('P'));
expect(screen.getByText('Modus')).toBeInTheDocument();
expect(screen.getByText('Wie gearbeitet wird')).toBeInTheDocument();
expect(screen.getByRole('option', { name: 'Schnell' })).toBeInTheDocument();
// No translation: shown as written.
expect(screen.getByRole('option', { name: 'careful' })).toBeInTheDocument();
expect(screen.getByText('Plain label')).toBeInTheDocument();
});
});
+12 -3
View File
@@ -1,7 +1,7 @@
'use client';
import { useState, useEffect, useRef } from 'react';
import { useTranslations } from 'next-intl';
import { useLocale, useTranslations } from 'next-intl';
import { usePluginStore } from '@/stores/plugin-store';
import { usePolicyStore } from '@/stores/policy-store';
import { useConfig } from '@/hooks/use-config';
@@ -14,6 +14,7 @@ import { ConfirmDialog } from '@/components/ui/confirm-dialog';
import { PluginPermissionList } from '@/components/plugins/plugin-permission-list';
import type { InstalledPlugin, PluginStatus, SettingFieldSchema } from '@/lib/plugin-types';
import { effectiveSettings } from '@/lib/plugin-settings';
import { lookup } from '@/lib/plugin-sandbox/plugin-i18n';
import { forgetCredentials, listConnectedAccounts, type ConnectedAccount } from '@/lib/plugin-sandbox/connect-dialog';
const STATUS_COLORS: Record<PluginStatus, string> = {
@@ -211,6 +212,7 @@ interface PluginCardProps {
function PluginCard({ plugin, isExpanded, isForceEnabled, controlsDisabled, onToggleExpand, onToggle, onRemove, onUpdateSettings }: PluginCardProps) {
const t = useTranslations('plugins.settings');
const locale = useLocale();
const isManaged = Boolean(plugin.managed);
const awaiting = !isManaged && plugin.approvalStatus === 'pending';
const denied = !isManaged && plugin.approvalStatus === 'denied';
@@ -308,6 +310,7 @@ function PluginCard({ plugin, isExpanded, isForceEnabled, controlsDisabled, onTo
key={key}
fieldKey={key}
schema={schema}
translate={(text) => (plugin.locales ? lookup(plugin.locales, locale, text) ?? text : text)}
value={effectiveSettings(plugin)[key]}
onChange={(value) => onUpdateSettings({ [key]: value })}
/>
@@ -381,12 +384,18 @@ function ConnectedAccounts({ pluginId }: { pluginId: string }) {
interface PluginSettingFieldProps {
fieldKey: string;
schema: SettingFieldSchema;
/**
* A label, description or option that names a key in the plugin's own
* translations (manifest `locales`) is shown translated; other text as is.
*/
translate?: (text: string) => string;
value: unknown;
onChange: (value: unknown) => void;
}
function PluginSettingField({ fieldKey, schema, value, onChange }: PluginSettingFieldProps) {
function PluginSettingField({ fieldKey, schema: raw, value, onChange, translate = (text) => text }: PluginSettingFieldProps) {
const inputId = `plugin-setting-${fieldKey}`;
const schema = { ...raw, label: translate(raw.label), ...(raw.description ? { description: translate(raw.description) } : {}) };
switch (schema.type) {
case 'boolean':
return (
@@ -413,7 +422,7 @@ function PluginSettingField({ fieldKey, schema, value, onChange }: PluginSetting
className="rounded border border-border bg-background px-2 py-1 text-xs text-foreground"
>
{(Array.isArray(schema.options) ? schema.options : []).map(opt => (
<option key={opt} value={opt}>{opt}</option>
<option key={opt} value={opt}>{translate(opt)}</option>
))}
</select>
</div>