From 997a41293ef7de9bdef1546634cb7e549f9c1331 Mon Sep 17 00:00:00 2001 From: Paul Nothaft Date: Thu, 18 Jun 2026 21:16:30 +0200 Subject: [PATCH] fix(i18n): sweep Events / API Tokens / Webhooks settings tabs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Continuing the activity-type i18n sweep from this PR: three settings tabs still had hardcoded English strings (or referenced i18n keys that didn't exist in either locale). EventsTab (Settings → Event Creation): - defaultFeedbackEnabled + defaultFeedbackEnabledHelp were referenced by the component but missing from both locales. The inline-default English text leaked through to German users. ApiTokensTab (Settings → API Tokens): - "Preview" table-header column was a bare string literal; now wraps through t('settings.apiTokens.preview'). - confirmRevoke called t() with a backtick template-literal default ("Revoke \"${token.name}\"…"). The interpolation happened at the default-string level, so the actual translated string never received the name and shipped without it. Switched to the i18next {{name}} parameter pattern with the matching value in en+de. WebhooksTab (Settings → Webhooks): - Half the tab was still hardcoded English. Wired everything through t(): toast messages (createError, updateError, deletedToast, deleteError, copied, copyFailed), Just-Created Secret card buttons (Copy, Dismiss), form placeholders (name, URL, template), advanced toggle label, filter and template help paragraphs, the filterError setter, all six table headers, the eventsSubscribed count (with proper {{count}} pluralisation), the status badge (Active/Disabled), the active/inactive title tooltips, the Deliveries link, the Delete button, and the delete-confirm dialog (proper {{name}} interpolation instead of the broken template-literal-in-default-string pattern). Added 34 new key/value pairs to each locale; counts now symmetric at events=28, apiTokens=23, webhooks=43 in both EN and DE. DE wording authored natively; tone matches the existing maintainer- voice style. --- .../features/settings/tabs/ApiTokensTab.tsx | 4 +- .../features/settings/tabs/WebhooksTab.tsx | 54 +++++++++---------- frontend/src/i18n/locales/de.json | 40 ++++++++++++-- frontend/src/i18n/locales/en.json | 40 ++++++++++++-- 4 files changed, 103 insertions(+), 35 deletions(-) diff --git a/frontend/src/features/settings/tabs/ApiTokensTab.tsx b/frontend/src/features/settings/tabs/ApiTokensTab.tsx index b1151c0b..6ee44fcb 100644 --- a/frontend/src/features/settings/tabs/ApiTokensTab.tsx +++ b/frontend/src/features/settings/tabs/ApiTokensTab.tsx @@ -183,7 +183,7 @@ export const ApiTokensTab: React.FC = () => { {t('settings.apiTokens.name', 'Name')} {t('settings.apiTokens.scopes', 'Scopes')} - Preview + {t('settings.apiTokens.preview', 'Preview')} {t('settings.apiTokens.lastUsed', 'Last used')} {t('settings.apiTokens.created', 'Created')} {t('settings.apiTokens.status', 'Status')} @@ -228,7 +228,7 @@ export const ApiTokensTab: React.FC = () => { variant="ghost" leftIcon={} onClick={() => { - if (confirm(t('settings.apiTokens.confirmRevoke', `Revoke "${token.name}"? Existing integrations using this token will start getting 401.`))) { + if (confirm(t('settings.apiTokens.confirmRevoke', { name: token.name, defaultValue: `Revoke "${token.name}"? Existing integrations using this token will start getting 401.` }))) { revokeMutation.mutate(token.id); } }} diff --git a/frontend/src/features/settings/tabs/WebhooksTab.tsx b/frontend/src/features/settings/tabs/WebhooksTab.tsx index 3e904028..a31454c8 100644 --- a/frontend/src/features/settings/tabs/WebhooksTab.tsx +++ b/frontend/src/features/settings/tabs/WebhooksTab.tsx @@ -67,7 +67,7 @@ export const WebhooksTab: React.FC = () => { try { parsedFilter = JSON.parse(trimmed); } catch { - setFilterError('Filter must be valid JSON'); + setFilterError(t('settings.webhooks.filterError', 'Filter must be valid JSON')); throw new Error('Invalid filter JSON'); } } @@ -89,7 +89,7 @@ export const WebhooksTab: React.FC = () => { queryClient.invalidateQueries({ queryKey: ['admin-webhooks'] }); }, onError: (err: any) => { - toast.error(err?.response?.data?.errors?.[0]?.msg || err?.response?.data?.error || 'Failed to create webhook'); + toast.error(err?.response?.data?.errors?.[0]?.msg || err?.response?.data?.error || t('settings.webhooks.createError', 'Failed to create webhook')); }, }); @@ -97,16 +97,16 @@ export const WebhooksTab: React.FC = () => { mutationFn: async ({ id, active }: { id: number; active: boolean }) => api.put(`/admin/webhooks/${id}`, { active }), onSuccess: () => queryClient.invalidateQueries({ queryKey: ['admin-webhooks'] }), - onError: () => toast.error('Failed to update webhook'), + onError: () => toast.error(t('settings.webhooks.updateError', 'Failed to update webhook')), }); const deleteMutation = useMutation({ mutationFn: async (id: number) => api.delete(`/admin/webhooks/${id}`), onSuccess: () => { - toast.success('Webhook deleted'); + toast.success(t('settings.webhooks.deletedToast', 'Webhook deleted')); queryClient.invalidateQueries({ queryKey: ['admin-webhooks'] }); }, - onError: () => toast.error('Failed to delete webhook'), + onError: () => toast.error(t('settings.webhooks.deleteError', 'Failed to delete webhook')), }); const toggleEvent = (e: WebhookEventType) => { @@ -165,16 +165,16 @@ export const WebhooksTab: React.FC = () => { onClick={async () => { try { await navigator.clipboard.writeText(justCreatedSecret); - toast.success('Copied'); + toast.success(t('settings.webhooks.copied', 'Copied')); } catch { - toast.error('Copy failed'); + toast.error(t('settings.webhooks.copyFailed', 'Copy failed')); } }} > - Copy + {t('settings.webhooks.copyButton', 'Copy')} @@ -188,13 +188,13 @@ export const WebhooksTab: React.FC = () => { - setName(e.target.value)} placeholder="e.g. n8n WhatsApp" /> + setName(e.target.value)} placeholder={t('settings.webhooks.namePlaceholder', 'e.g. n8n WhatsApp')} />
- setUrl(e.target.value)} placeholder="https://n8n.example.com/webhook/picpeak" /> + setUrl(e.target.value)} placeholder={t('settings.webhooks.urlPlaceholder', 'https://n8n.example.com/webhook/picpeak')} />
@@ -222,7 +222,7 @@ export const WebhooksTab: React.FC = () => { onClick={() => setShowAdvanced((prev) => !prev)} className="text-sm text-primary-600 dark:text-primary-400 hover:underline self-start" > - {showAdvanced ? '− Hide advanced (filter, template)' : '+ Advanced (filter, template)'} + {showAdvanced ? t('settings.webhooks.hideAdvanced', '− Hide advanced (filter, template)') : t('settings.webhooks.showAdvanced', '+ Advanced (filter, template)')} {showAdvanced && ( @@ -239,7 +239,7 @@ export const WebhooksTab: React.FC = () => { className="w-full px-3 py-2 border border-neutral-300 dark:border-neutral-700 dark:bg-neutral-800 rounded text-sm font-mono" />

- Dot-path → expected value. All keys must match (AND). Use an array for "any of": {'{"type": ["event.published", "event.archived"]}'} + {t('settings.webhooks.filterHelp', 'Dot-path → expected value. All keys must match (AND). Use an array for "any of".')} {'{"type": ["event.published", "event.archived"]}'}

{filterError &&

{filterError}

} @@ -251,12 +251,12 @@ export const WebhooksTab: React.FC = () => {