mirror of
https://github.com/bulwarkmail/webmail.git
synced 2026-10-31 00:12:39 +00:00
feat: show plugin setting labels in the user's language when the plugin translates them
This commit is contained in:
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user