From 44f5246e38243bb0b71526c12fe260891de9e91e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Padez?= Date: Sun, 23 Aug 2026 05:04:22 +0000 Subject: [PATCH] server settings keyed (352 -> 267) ServerSettings reaches zero: OCR, STT, TTS, SMTP, maintenance and the AI harnesses. Two more hand-rolled plurals became real plural entries -- maintenance said "commit" with a ternary on length === 1, and both read correctly at 0, 1 and 2 in both locales now. The three Cancel buttons in AIHarnessesSection take ui.button.cancel rather than adding a fourth app-level copy of the word, which is what phase 1 put it there for. Scanner: a single token carrying digits and punctuation is a name, not a sentence -- the OCR placeholder is the example model file Qwen2.5-VL-7B-Instruct-q4_k_m.gguf. Prose has spaces, and requiring whitespace to be absent is what keeps this from eating real one-word labels. Verified the delta is exactly that one string. Note for the owner: another session has src/servers/api/sub-agents/router.ts in flight with three type errors. Untouched by me and excluded from my gate; the full tree will not typecheck clean until they land it. --- scripts/i18n/scan-hardcoded.ts | 4 + .../ServerSettings/AIHarnessesSection.tsx | 76 +++++++++------- .../ServerSettings/MaintenanceSection.tsx | 22 ++--- .../Settings/ServerSettings/OCRSection.tsx | 18 ++-- .../Settings/ServerSettings/SMTPSection.tsx | 48 +++++----- .../Settings/ServerSettings/STTSection.tsx | 12 +-- .../Settings/ServerSettings/TTSSection.tsx | 26 +++--- src/apps/officer-web/locales/en-GB.json | 89 +++++++++++++++++++ src/apps/officer-web/locales/pt-PT.json | 89 +++++++++++++++++++ 9 files changed, 296 insertions(+), 88 deletions(-) diff --git a/scripts/i18n/scan-hardcoded.ts b/scripts/i18n/scan-hardcoded.ts index 8b09b8b3..7dc5711a 100755 --- a/scripts/i18n/scan-hardcoded.ts +++ b/scripts/i18n/scan-hardcoded.ts @@ -85,6 +85,10 @@ function isHuman(value: string | undefined): value is string { // pattern because it starts with a capital and interpolates; excluded here because an auth scheme // followed by a credential is never something a person reads. if (/^(Bearer|Basic|Token|Digest)\s/.test(s)) return false; + // A single token carrying digits AND punctuation is a name, not a sentence — the OCR section's + // placeholder is the example model file `Qwen2.5-VL-7B-Instruct-q4_k_m.gguf`. Prose has spaces; the + // whitespace test is what keeps this from eating real one-word labels, which never look like this. + if (!/\s/.test(s) && /\d/.test(s) && /[-_.]/.test(s)) return false; // Tailwind, reached via a `description:`/`label:` key in a classNames object — e.g. sonner's // `description: 'group-[.toast]:text-muted-foreground'`. // diff --git a/src/apps/officer-web/Screens/Dashboard/Settings/ServerSettings/AIHarnessesSection.tsx b/src/apps/officer-web/Screens/Dashboard/Settings/ServerSettings/AIHarnessesSection.tsx index e3ea139c..d81e6892 100644 --- a/src/apps/officer-web/Screens/Dashboard/Settings/ServerSettings/AIHarnessesSection.tsx +++ b/src/apps/officer-web/Screens/Dashboard/Settings/ServerSettings/AIHarnessesSection.tsx @@ -13,6 +13,7 @@ import { CommandList, } from '@/components/ui/command'; import { useClient } from 'hooks/useClient'; +import { useTranslation } from '@/lib/i18n'; type VersionInfo = { version: string | null; path: string | null; globalPath: string | null }; type StoredApiKeys = { keys: { provider: string; value: string }[] }; @@ -38,6 +39,7 @@ type ChatProvider = { key: string; providerId: string }; type ProbeState = { step: 'url' } | { step: 'probing' } | { step: 'auth'; probe: ProbeResult } | { step: 'saving' }; export const AIHarnessesSection = () => { + const { t, tUi } = useTranslation(); const client = useClient(); const queryClient = useQueryClient(); const [installing, setInstalling] = useState(false); @@ -168,7 +170,7 @@ export const AIHarnessesSection = () => { // No auth needed — save directly await saveLocalProvider(result, auth); } catch { - toast.error('Failed to connect'); + toast.error(t('settings.server.harnesses.connectFailed')); setProbeState({ step: 'url' }); } }; @@ -192,13 +194,13 @@ export const AIHarnessesSection = () => { return; } if (result.needsAuth) { - toast.error('Authentication failed'); + toast.error(t('settings.server.harnesses.authFailed')); setProbeState({ step: 'auth', probe }); return; } await saveLocalProvider(result, auth); } catch { - toast.error('Failed to connect'); + toast.error(t('settings.server.harnesses.connectFailed')); setProbeState({ step: 'auth', probe }); } }; @@ -218,10 +220,10 @@ export const AIHarnessesSection = () => { queryClient.invalidateQueries({ queryKey: ['CHAT_PROVIDERS_LOCAL_PROVIDERS'] }); queryClient.invalidateQueries({ queryKey: ['CHAT_PROVIDERS_LOCAL_HEALTH'] }); queryClient.invalidateQueries({ queryKey: ['CHAT_MODELS'] }); - toast.success(`Connected to ${probe.name}`); + toast.success(t('settings.server.harnesses.connectedTo', { name: probe.name ?? '' })); resetLocalForm(); } catch { - toast.error('Failed to save provider'); + toast.error(t('settings.server.harnesses.saveFailed')); setProbeState({ step: 'url' }); } }; @@ -254,9 +256,9 @@ export const AIHarnessesSection = () => {
- Pi Mono + {t('settings.server.harnesses.piMono')} {piMonoLoading ? ( - Checking... + {t('settings.server.harnesses.checking')} ) : piMonoVersion?.version ? (
{piMonoVersion.version} @@ -265,7 +267,7 @@ export const AIHarnessesSection = () => { onClick={installAgent} disabled={installing} className="p-0.5 rounded hover:bg-duck-teal/10 cursor-pointer transition-colors disabled:opacity-30" - title="Update Pi Mono" + title={t('settings.server.harnesses.updatePiMono')} > @@ -285,13 +287,21 @@ export const AIHarnessesSection = () => { <> {/* Local Providers */}
- Local Providers + + {t('settings.server.harnesses.localProviders')} +
{localProviders.map((lp: LocalProviderEntry) => (
{lp.name} {lp.url} @@ -299,7 +309,7 @@ export const AIHarnessesSection = () => { type="button" onClick={() => removeLocalProvider(lp.id)} className="shrink-0 p-0.5 rounded hover:bg-red-500/10 cursor-pointer transition-colors" - title="Remove provider" + title={t('settings.server.harnesses.removeProvider')} > @@ -312,7 +322,7 @@ export const AIHarnessesSection = () => { setLocalName(ev.target.value)} onKeyDown={(ev) => { @@ -340,7 +350,7 @@ export const AIHarnessesSection = () => { className="h-7 text-xs bg-duck-dark text-white hover:bg-duck-dark/90 dark:bg-foreground dark:text-background dark:hover:bg-foreground/90 cursor-pointer disabled:opacity-40" disabled={!localUrl.trim()} > - Connect + {t('settings.server.harnesses.connect')} )} {(probeState.step === 'probing' || probeState.step === 'saving') && ( @@ -351,7 +361,7 @@ export const AIHarnessesSection = () => { onClick={resetLocalForm} className="text-duck-dark/40 hover:text-duck-dark/70 dark:text-foreground/40 dark:hover:text-foreground/70 cursor-pointer transition-colors text-xs" > - Cancel + {tUi('button.cancel')}
@@ -370,7 +380,7 @@ export const AIHarnessesSection = () => { setAuthUsername(ev.target.value)} autoFocus @@ -378,7 +388,7 @@ export const AIHarnessesSection = () => { setAuthPassword(ev.target.value)} /> @@ -388,7 +398,7 @@ export const AIHarnessesSection = () => { className="h-7 text-xs w-fit bg-duck-dark text-white hover:bg-duck-dark/90 dark:bg-foreground dark:text-background dark:hover:bg-foreground/90 cursor-pointer disabled:opacity-40" disabled={!authUsername || !authPassword} > - Authenticate + {t('settings.server.harnesses.authenticate')} ) : ( @@ -396,7 +406,7 @@ export const AIHarnessesSection = () => { setAuthApiKey(ev.target.value)} autoFocus @@ -407,7 +417,7 @@ export const AIHarnessesSection = () => { className="h-7 text-xs w-fit bg-duck-dark text-white hover:bg-duck-dark/90 dark:bg-foreground dark:text-background dark:hover:bg-foreground/90 cursor-pointer disabled:opacity-40" disabled={!authApiKey} > - Authenticate + {t('settings.server.harnesses.authenticate')} )} @@ -423,7 +433,7 @@ export const AIHarnessesSection = () => { onClick={() => setAddingLocal(true)} > - Connect Local Provider + {t('settings.server.harnesses.connectLocal')} )}
@@ -431,7 +441,9 @@ export const AIHarnessesSection = () => { {/* Remote Providers */}
- Remote Providers + + {t('settings.server.harnesses.remoteProviders')} +
{connectedProviders.map((provider) => (
@@ -457,7 +469,7 @@ export const AIHarnessesSection = () => { type="button" onClick={() => setEditingProvider(editingProvider === provider.key ? null : provider.key)} className="shrink-0 p-0.5 rounded hover:bg-duck-teal/10 cursor-pointer transition-colors" - title="Edit key" + title={t('settings.server.harnesses.editKey')} > @@ -465,7 +477,7 @@ export const AIHarnessesSection = () => { type="button" onClick={() => disconnectProvider(provider)} className="shrink-0 p-0.5 rounded hover:bg-red-500/10 cursor-pointer transition-colors" - title="Disconnect provider" + title={t('settings.server.harnesses.disconnect')} > @@ -478,7 +490,7 @@ export const AIHarnessesSection = () => { setKeyInputs((prev) => ({ ...prev, [provider.providerId]: ev.target.value })) @@ -492,7 +504,7 @@ export const AIHarnessesSection = () => { type="submit" disabled={keyInputs[provider.providerId] === undefined || savingKey === provider.providerId} className="shrink-0 p-1 rounded hover:bg-duck-teal/10 cursor-pointer transition-colors disabled:opacity-30 disabled:cursor-default" - title="Save key" + title={t('settings.server.harnesses.saveKey')} > @@ -501,7 +513,7 @@ export const AIHarnessesSection = () => { onClick={() => setEditingProvider(null)} className="text-duck-dark/40 hover:text-duck-dark/70 dark:text-foreground/40 dark:hover:text-foreground/70 cursor-pointer transition-colors text-xs" > - Cancel + {tUi('button.cancel')} )} @@ -524,7 +536,7 @@ export const AIHarnessesSection = () => { setKeyInputs((prev) => ({ ...prev, [provider.providerId]: ev.target.value })) @@ -538,7 +550,7 @@ export const AIHarnessesSection = () => { type="submit" disabled={keyInputs[provider.providerId] === undefined || savingKey === provider.providerId} className="shrink-0 p-1 rounded hover:bg-duck-teal/10 cursor-pointer transition-colors disabled:opacity-30 disabled:cursor-default" - title="Save key" + title={t('settings.server.harnesses.saveKey')} > @@ -547,7 +559,7 @@ export const AIHarnessesSection = () => { onClick={() => setEditingProvider(null)} className="text-duck-dark/40 hover:text-duck-dark/70 dark:text-foreground/40 dark:hover:text-foreground/70 cursor-pointer transition-colors text-xs" > - Cancel + {tUi('button.cancel')} ); @@ -561,14 +573,14 @@ export const AIHarnessesSection = () => { onClick={() => setCommandOpen(true)} > - Connect Provider + {t('settings.server.harnesses.connectProvider')} )}
- + - No providers found. + {t('settings.server.harnesses.none')} {unconnectedProviders.map((provider) => ( { + const { t } = useTranslation(); const client = useClient(); const [pulling, setPulling] = useState(false); const [pulled, setPulled] = useState<{ from: string; to: string; commits: string[]; needs: string[] } | null>(null); @@ -58,11 +60,11 @@ export const MaintenanceSection = () => { }>('/server-settings/pull', {}); if (!res.updated) { setPulled(null); - toast.success('Already up to date'); + toast.success(t('settings.server.maintenance.upToDate')); return; } setPulled({ from: res.from ?? '', to: res.to ?? '', commits: res.commits, needs: res.needs }); - toast.success(`Pulled ${res.commits.length} commit${res.commits.length === 1 ? '' : 's'}`); + toast.success(t('settings.server.maintenance.pulled', { count: res.commits.length })); } catch (err) { toast.error(errorMessage(err, 'Pull failed')); } finally { @@ -81,7 +83,7 @@ export const MaintenanceSection = () => { setLastBuild(`${(res.ms / 1000).toFixed(1)}s · ${res.outputs} files`); // Nothing reloads itself: the bundle on disk changed, this page is still the old one. Saying so is // the difference between "it worked" and "why does my change not show". - toast.success('Frontend rebuilt — refresh this page to see it'); + toast.success(t('settings.server.maintenance.rebuilt')); } catch (err) { // The client throws on any status >= 400 with the raw response body as its message, so a build // failure arrives here as the JSON the route sent. Unwrapped, or the toast shows a reader @@ -96,7 +98,7 @@ export const MaintenanceSection = () => { setRestarting(true); try { await client.post('/server-settings/restart', {}); - toast.success('Restarting — the page will reconnect on its own'); + toast.success(t('settings.server.maintenance.restarting')); } catch (err) { /** * Two failures arrive here and they are opposites. @@ -116,7 +118,7 @@ export const MaintenanceSection = () => { setRestarting(false); return; } - toast.success('Restarting — the page will reconnect on its own'); + toast.success(t('settings.server.maintenance.restarting')); } // Left disabled on the success path, deliberately: the button must not become pressable again while // the server is coming back. @@ -126,14 +128,12 @@ export const MaintenanceSection = () => {
-
Update the platform
+
{t('settings.server.maintenance.updateTitle')}

Pulls this server’s repository. Refuses if anything is uncommitted or the checkout has diverged — it will never discard local work.

-

- Changes nothing that is running. Whatever the update needs is listed here, and stays your call. -

+

{t('settings.server.maintenance.updateHint')}

{pulled && (

@@ -163,7 +163,7 @@ export const MaintenanceSection = () => {

-
Rebuild the frontend
+
{t('settings.server.maintenance.rebuild')}

Picks up changes to the interface. Nothing restarts, no connection drops, and this chat keeps its session — refresh the page when it finishes. @@ -178,7 +178,7 @@ export const MaintenanceSection = () => {

-
Restart the server
+
{t('settings.server.maintenance.restart')}

The only way to load changed server code — Bun caches modules for the life of the process, so rebuilding cannot see an edit to the backend. diff --git a/src/apps/officer-web/Screens/Dashboard/Settings/ServerSettings/OCRSection.tsx b/src/apps/officer-web/Screens/Dashboard/Settings/ServerSettings/OCRSection.tsx index 440f61a6..f2d5e16d 100644 --- a/src/apps/officer-web/Screens/Dashboard/Settings/ServerSettings/OCRSection.tsx +++ b/src/apps/officer-web/Screens/Dashboard/Settings/ServerSettings/OCRSection.tsx @@ -8,6 +8,7 @@ import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; import { useClient } from 'hooks/useClient'; +import { useTranslation } from '@/lib/i18n'; type OcrConfig = { url: string; @@ -15,6 +16,7 @@ type OcrConfig = { }; export const OCRSection = () => { + const { t } = useTranslation(); const client = useClient(); const queryClient = useQueryClient(); @@ -59,9 +61,9 @@ export const OCRSection = () => { try { await client.put('/server-settings/ocr', { url, model }); queryClient.invalidateQueries({ queryKey: ['OCR_CONFIG'] }); - toast.success('OCR settings saved'); + toast.success(t('settings.server.ocr.saved')); } catch { - toast.error('Failed to save OCR settings'); + toast.error(t('settings.server.ocr.saveFailed')); } finally { setIsSaving(false); } @@ -75,7 +77,7 @@ export const OCRSection = () => { if (res.error) { toast.error(res.error); } else { - toast.success('Vision model server is reachable'); + toast.success(t('settings.server.ocr.reachable')); } } catch (err: unknown) { const raw = (err as { message?: string })?.message; @@ -91,12 +93,12 @@ export const OCRSection = () => { } }; - if (isLoading) return

Loading...

; + if (isLoading) return

{t('settings.server.loading')}

; return (