diff --git a/src/apps/officer-web/Screens/Dashboard/Settings/AISettings.tsx b/src/apps/officer-web/Screens/Dashboard/Settings/AISettings.tsx index 1b85b0bd..b72b4d39 100644 --- a/src/apps/officer-web/Screens/Dashboard/Settings/AISettings.tsx +++ b/src/apps/officer-web/Screens/Dashboard/Settings/AISettings.tsx @@ -11,6 +11,7 @@ import { SettingsRoute, createSettingsPanelComponents, type SettingsSectionGroup import { useSettings } from 'state/useSettings'; import type { UserSettings } from 'state/useSettings'; import { useModels, useVisibleModels, modelKey, getProviderDisplayName, type ModelOption } from 'state/useModels'; +import { useTranslation } from '@/lib/i18n'; import { useAccessPolicy } from 'state/useAccessPolicy'; import { useUserState } from 'state/useUserState'; import { AIHarnessesSection } from './ServerSettings/AIHarnessesSection'; @@ -104,6 +105,7 @@ const DropZone = ({ label, children, onDrop }: DropZoneProps) => { // --- My Models Section (per-user hidden models) --- function MyModelsSection() { + const { t } = useTranslation(); const { settings, saveSettings } = useSettings(); const visibleModels = useModels(); const [activeProvider, setActiveProvider] = useUserState('my-models-provider', ''); @@ -174,11 +176,7 @@ function MyModelsSection() { }, []); if (providerGroups.length === 0) { - return ( -

- No models available. Configure API keys in Providers. -

- ); + return

{t('settings.ai.noModels')}

; } return ( @@ -199,9 +197,9 @@ function MyModelsSection() { ))} - + {shown.length === 0 && ( - Drag models here to show + {t('settings.ai.dragToShow')} )} {shown.map((m) => ( - + {hidden.length === 0 && ( - All models visible + {t('settings.ai.allVisible')} )} {hidden.map((m) => ( - No models available. Configure API keys in Providers. -

- ); + return

{t('settings.ai.noModels')}

; } return ( @@ -353,15 +348,17 @@ function MemberModelsSection() { onClick={refreshModels} disabled={refreshing} className="ml-auto p-1.5 rounded hover:bg-duck-teal/10 cursor-pointer transition-colors disabled:opacity-30" - title="Refresh models from providers" + title={t('settings.ai.refresh')} > - + {enabled.length === 0 && ( - Drag models here to enable + + {t('settings.ai.dragToEnable')} + )} {enabled.map((m) => ( - + {disabled.length === 0 && ( - All models enabled + {t('settings.ai.allEnabled')} )} {disabled.map((m) => ( { const modelsGroup: SettingsSectionGroup = { - label: 'Models', + labelKey: 'settings.groups.models', icon: Eye, sections: [ { key: 'my-models', icon: Eye, - title: 'My Models', - description: 'Show or hide models for yourself', + titleKey: 'settings.sections.myModels.label', + descriptionKey: 'settings.sections.myModels.description', content: , }, { key: 'member-models', icon: Eye, - title: 'Channel Models', - description: 'Models reachable by non-owner accounts', + titleKey: 'settings.sections.channelModels.label', + descriptionKey: 'settings.sections.channelModels.description', content: , }, ], }; const defaultsGroup: SettingsSectionGroup = { - label: 'Defaults', + labelKey: 'settings.groups.defaults', icon: Sparkles, sections: [ { key: 'default-models', icon: Sparkles, - title: 'Default Models', - description: 'Default models for chat, projects, and tasks', + titleKey: 'settings.sections.defaultModels.label', + descriptionKey: 'settings.sections.defaultModels.description', content: , }, ], }; const providersGroup: SettingsSectionGroup = { - label: 'Providers', + labelKey: 'settings.groups.providers', icon: Terminal, sections: [ { key: 'ai-harnesses', icon: Terminal, - title: 'Providers', - description: 'Remote and local AI providers', + titleKey: 'settings.sections.providers.label', + descriptionKey: 'settings.sections.providers.description', content: , }, ], @@ -482,7 +479,7 @@ export const AISettings = () => { createSettingsPanelComponents({ basePath: BASE_PATH, sidebarIcon: Bot, - sidebarLabel: 'AI', + sidebarLabelKey: 'settings.nav.ai', groups, }), [groups], diff --git a/src/apps/officer-web/Screens/Dashboard/Settings/IntegrationsSettings/index.tsx b/src/apps/officer-web/Screens/Dashboard/Settings/IntegrationsSettings/index.tsx index cc763eae..2d5b4453 100644 --- a/src/apps/officer-web/Screens/Dashboard/Settings/IntegrationsSettings/index.tsx +++ b/src/apps/officer-web/Screens/Dashboard/Settings/IntegrationsSettings/index.tsx @@ -10,6 +10,7 @@ import { GoogleOAuthConfig } from './GoogleOAuthConfig'; // import { GoogleAccount } from './GoogleAccount'; // hidden — email uses IMAP app password now import { ApifyConfig } from './ApifyConfig'; import { DavAppPasswords } from './DavAppPasswords'; +import { useTranslation } from '@/lib/i18n'; const BASE_PATH = '/settings/integrations'; @@ -17,15 +18,15 @@ const enterpriseSections: SettingsSection[] = [ { key: 'google-oauth', icon: KeyRound, - title: 'Google OAuth', - description: 'Client ID and secret for Google APIs', + titleKey: 'settings.sections.googleOauth.label', + descriptionKey: 'settings.sections.googleOauth.description', content: , }, { key: 'apify', icon: Wrench, - title: 'Apify', - description: 'API token for web scraping actors', + titleKey: 'settings.sections.apify.label', + descriptionKey: 'settings.sections.apify.description', content: , }, ]; @@ -39,14 +40,14 @@ const personalSections: SettingsSection[] = [ // key: 'google-account', // icon: UserCircle, // title: 'Google Account', - // description: 'Connect your Google account', + // descriptionKey: 'settings.sections.apify.description', // content: , // }, { key: 'dav-app-passwords', icon: CalendarDays, - title: 'Calendar & Contacts sync', - description: 'Per-device passwords for CalDAV/CardDAV clients', + titleKey: 'settings.sections.davSync.label', + descriptionKey: 'settings.sections.davSync.description', content: , }, ]; @@ -60,6 +61,7 @@ const tabForSection = (section: string | undefined) => personalSections.some((s) => s.key === section) ? 'personal' : 'enterprise'; const IntegrationsSidebar = () => { + const { t } = useTranslation(); const { section } = useParams<{ section?: string }>(); const navigate = useNavigate(); const tab = tabForSection(section); @@ -77,22 +79,28 @@ const IntegrationsSidebar = () => {
- Integrations + {t('settings.nav.integrations')}
- Enterprise + {t('settings.integrations.enterprise')} - Personal + {t('settings.integrations.personal')}
- + ); }; diff --git a/src/apps/officer-web/Screens/Dashboard/Settings/ProfileSettings/index.tsx b/src/apps/officer-web/Screens/Dashboard/Settings/ProfileSettings/index.tsx index 0e154bc6..057d6efb 100644 --- a/src/apps/officer-web/Screens/Dashboard/Settings/ProfileSettings/index.tsx +++ b/src/apps/officer-web/Screens/Dashboard/Settings/ProfileSettings/index.tsx @@ -14,12 +14,18 @@ import { VoicePreference } from './VoicePreference'; const BASE_PATH = '/settings/profile'; const sections: SettingsSection[] = [ - { key: 'profile', icon: User, title: 'Profile', description: 'Avatar, name, and email', content: }, + { + key: 'profile', + icon: User, + titleKey: 'settings.sections.profile.label', + descriptionKey: 'settings.sections.profile.description', + content: , + }, { key: 'change-password', icon: Lock, - title: 'Change Password', - description: 'Update your password', + titleKey: 'settings.sections.changePassword.label', + descriptionKey: 'settings.sections.changePassword.description', content: , }, // Beside the password rather than under Integrations, where it was until 2026-08-21. A key is not an @@ -29,29 +35,29 @@ const sections: SettingsSection[] = [ { key: 'api-keys', icon: KeyRound, - title: 'API keys', - description: 'Keys that sign in as you, revocable one at a time', + titleKey: 'settings.sections.apiKeys.label', + descriptionKey: 'settings.sections.apiKeys.description', content: , }, { key: 'languages', icon: Globe, - title: 'Languages', - description: 'Spoken, default, and translation', + titleKey: 'settings.sections.languages.label', + descriptionKey: 'settings.sections.languages.description', content: , }, { key: 'voice', icon: Volume2, - title: 'Voice', - description: 'Text-to-speech voice preference', + titleKey: 'settings.sections.voice.label', + descriptionKey: 'settings.sections.voice.description', content: , }, { key: 'dock', icon: LayoutGrid, - title: 'Dock', - description: 'Choose and reorder dock items', + titleKey: 'settings.sections.dock.label', + descriptionKey: 'settings.sections.dock.description', content: , }, ]; @@ -59,7 +65,7 @@ const sections: SettingsSection[] = [ const { Sidebar, Content } = createSettingsPanelComponents({ basePath: BASE_PATH, sidebarIcon: User, - sidebarLabel: 'Profile', + sidebarLabelKey: 'settings.nav.profile', sections, }); diff --git a/src/apps/officer-web/Screens/Dashboard/Settings/SettingsPanel.tsx b/src/apps/officer-web/Screens/Dashboard/Settings/SettingsPanel.tsx index a64577d3..410c045e 100644 --- a/src/apps/officer-web/Screens/Dashboard/Settings/SettingsPanel.tsx +++ b/src/apps/officer-web/Screens/Dashboard/Settings/SettingsPanel.tsx @@ -3,17 +3,24 @@ import { useState } from 'react'; import { NavLink, Navigate, useParams } from 'react-router'; import type { LucideIcon } from 'lucide-react'; import { Input } from '@/components/ui/input'; +import { useTranslation, type AppTranslationKey } from '@/lib/i18n'; +/** + * Sections are declared as module-level consts holding JSX `content`, so they cannot call a hook — + * they carry catalog KEYS and the renderers below resolve them. `titleKey` rather than `titleKeyName` + * and deliberately not `title_key`: docs-check-settings.ts greps `key:\s*'…'` for route segments, and + * a capital K keeps `titleKey` out of that match. + */ export type SettingsSection = { key: string; icon: LucideIcon; - title: string; - description: string; + titleKey: AppTranslationKey; + descriptionKey: AppTranslationKey; content: ReactNode; }; export type SettingsSectionGroup = { - label: string; + labelKey: AppTranslationKey; icon: LucideIcon; sections: SettingsSection[]; }; @@ -21,7 +28,7 @@ export type SettingsSectionGroup = { type SettingsSidebarProps = { basePath: string; icon: LucideIcon; - label: string; + labelKey: AppTranslationKey; sections: SettingsSection[]; groups?: SettingsSectionGroup[]; hideHeader?: boolean; @@ -32,44 +39,51 @@ type SettingsSidebarProps = { * the browser knows it has been visited, and `isActive` comes from the router instead of from a global * that the content pane had to be told about separately. */ -const SectionLink = ({ section, to }: { section: SettingsSection; to: string }) => ( - - `flex items-start gap-2.5 py-2 px-3 rounded-lg text-left cursor-pointer transition-colors ${ - isActive - ? 'bg-duck-teal/10 text-duck-dark dark:text-foreground' - : 'text-duck-dark/70 dark:text-foreground/70 hover:bg-duck-dark/5 dark:hover:bg-foreground/5 hover:text-duck-dark dark:hover:text-foreground' - }` - } - > - {({ isActive }) => ( - <> - -
-
{section.title}
-
{section.description}
-
- - )} -
-); +const SectionLink = ({ section, to }: { section: SettingsSection; to: string }) => { + const { t } = useTranslation(); + return ( + + `flex items-start gap-2.5 py-2 px-3 rounded-lg text-left cursor-pointer transition-colors ${ + isActive + ? 'bg-duck-teal/10 text-duck-dark dark:text-foreground' + : 'text-duck-dark/70 dark:text-foreground/70 hover:bg-duck-dark/5 dark:hover:bg-foreground/5 hover:text-duck-dark dark:hover:text-foreground' + }` + } + > + {({ isActive }) => ( + <> + +
+
{t(section.titleKey)}
+
+ {t(section.descriptionKey)} +
+
+ + )} +
+ ); +}; export const SettingsSidebar = ({ basePath, icon: Icon, - label, + labelKey, sections, groups, hideHeader, }: SettingsSidebarProps) => { const [search, setSearch] = useState(''); + const { t } = useTranslation(); const query = search.toLowerCase(); + // Matched against the RESOLVED words, so searching works in the language on screen. const matchesSearch = (s: SettingsSection) => - s.title.toLowerCase().includes(query) || s.description.toLowerCase().includes(query); + t(s.titleKey).toLowerCase().includes(query) || t(s.descriptionKey).toLowerCase().includes(query); return (
@@ -77,13 +91,13 @@ export const SettingsSidebar = ({
- {label} + {t(labelKey)}
)}
setSearch(ev.target.value)} className="h-8 text-xs" @@ -95,11 +109,11 @@ export const SettingsSidebar = ({ const filtered = group.sections.filter(matchesSearch); if (filtered.length === 0) return null; return ( -
+
- {group.label} + {t(group.labelKey)}
{filtered.map((s) => ( @@ -121,6 +135,7 @@ type SettingsContentProps = { }; export const SettingsContent = ({ sections }: SettingsContentProps) => { + const { t } = useTranslation(); // The sidebar and this pane are two sibling panels that used to agree via a shared `useGlobal` key. // They agree via the URL now, so neither has to be told about the other — and neither can be the one // that seeds the slot, which is what made the old defaulting order matter (see §5.6). @@ -130,7 +145,7 @@ export const SettingsContent = ({ sections }: SettingsContentProps) => { if (!section) { return (
-

Select a section

+

{t('settings.selectSection')}

); } @@ -139,9 +154,9 @@ export const SettingsContent = ({ sections }: SettingsContentProps) => {
-

{section.title}

+

{t(section.titleKey)}

-

{section.description}

+

{t(section.descriptionKey)}

{section.content}
); @@ -166,7 +181,7 @@ export const SettingsRoute = ({ basePath, sections, children }: SettingsRoutePro type CreateSettingsPanelParams = { basePath: string; sidebarIcon: LucideIcon; - sidebarLabel: string; + sidebarLabelKey: AppTranslationKey; sections?: SettingsSection[]; groups?: SettingsSectionGroup[]; }; @@ -174,7 +189,7 @@ type CreateSettingsPanelParams = { export const createSettingsPanelComponents = ({ basePath, sidebarIcon, - sidebarLabel, + sidebarLabelKey, sections = [], groups, }: CreateSettingsPanelParams) => { @@ -183,7 +198,7 @@ export const createSettingsPanelComponents = ({ diff --git a/src/apps/officer-web/Screens/Dashboard/Settings/SystemSettings.tsx b/src/apps/officer-web/Screens/Dashboard/Settings/SystemSettings.tsx index c8ae423c..d6a249b4 100644 --- a/src/apps/officer-web/Screens/Dashboard/Settings/SystemSettings.tsx +++ b/src/apps/officer-web/Screens/Dashboard/Settings/SystemSettings.tsx @@ -12,44 +12,56 @@ import { OCRSection } from './ServerSettings/OCRSection'; const groups: SettingsSectionGroup[] = [ { - label: 'AI', + labelKey: 'settings.groups.ai', icon: Bot, sections: [ { key: 'tts', icon: Volume2, - title: 'Text to Speech', - description: 'TTS provider and voice', + titleKey: 'settings.sections.tts.label', + descriptionKey: 'settings.sections.tts.description', content: , }, - { key: 'stt', icon: Mic, title: 'Speech to Text', description: 'Whisper server URL', content: }, + { + key: 'stt', + icon: Mic, + titleKey: 'settings.sections.stt.label', + descriptionKey: 'settings.sections.stt.description', + content: , + }, { key: 'ocr', icon: ScanText, - title: 'OCR', - description: 'Vision model for text extraction', + titleKey: 'settings.sections.ocr.label', + descriptionKey: 'settings.sections.ocr.description', content: , }, ], }, { - label: 'Maintenance', + labelKey: 'settings.groups.maintenance', icon: Hammer, sections: [ { key: 'maintenance', icon: Hammer, - title: 'Rebuild & restart', - description: 'Load changes without a terminal', + titleKey: 'settings.sections.maintenance.label', + descriptionKey: 'settings.sections.maintenance.description', content: , }, ], }, { - label: 'Email', + labelKey: 'settings.groups.email', icon: Mail, sections: [ - { key: 'smtp', icon: Mail, title: 'SMTP', description: 'Email delivery provider', content: }, + { + key: 'smtp', + icon: Mail, + titleKey: 'settings.sections.smtp.label', + descriptionKey: 'settings.sections.smtp.description', + content: , + }, ], }, ]; @@ -59,7 +71,7 @@ const BASE_PATH = '/settings/system'; const { Sidebar, Content, allSections } = createSettingsPanelComponents({ basePath: BASE_PATH, sidebarIcon: Settings, - sidebarLabel: 'System', + sidebarLabelKey: 'settings.nav.system', groups, }); diff --git a/src/apps/officer-web/Screens/Dashboard/Settings/UserManagement/index.tsx b/src/apps/officer-web/Screens/Dashboard/Settings/UserManagement/index.tsx index cfce829e..1ffc0ff2 100644 --- a/src/apps/officer-web/Screens/Dashboard/Settings/UserManagement/index.tsx +++ b/src/apps/officer-web/Screens/Dashboard/Settings/UserManagement/index.tsx @@ -15,15 +15,15 @@ const sections: SettingsSection[] = [ { key: 'users', icon: Users, - title: 'Accounts', - description: 'Who has access, and as what', + titleKey: 'settings.sections.accounts.label', + descriptionKey: 'settings.sections.accounts.description', content: , }, { key: 'permissions', icon: ShieldCheck, - title: 'Permissions', - description: 'What each role can reach', + titleKey: 'settings.sections.permissions.label', + descriptionKey: 'settings.sections.permissions.description', content: , }, ]; @@ -33,7 +33,7 @@ const BASE_PATH = '/settings/user-management'; const { Sidebar, Content } = createSettingsPanelComponents({ basePath: BASE_PATH, sidebarIcon: UserCog, - sidebarLabel: 'Users', + sidebarLabelKey: 'settings.nav.users', sections, }); diff --git a/src/apps/officer-web/locales/en-GB.json b/src/apps/officer-web/locales/en-GB.json index 7c58000a..45a4aa5a 100644 --- a/src/apps/officer-web/locales/en-GB.json +++ b/src/apps/officer-web/locales/en-GB.json @@ -50,74 +50,129 @@ "sections": { "profile": { "label": "Profile", - "hint": "avatar, name, email" + "hint": "avatar, name, email", + "description": "Avatar, name, and email" }, "changePassword": { - "label": "Change password" + "label": "Change password", + "description": "Update your password" }, "apiKeys": { "label": "API keys", - "hint": "keys that sign in as you" + "hint": "keys that sign in as you", + "description": "Keys that sign in as you, revocable one at a time" }, "languages": { "label": "Languages", - "hint": "spoken and translation" + "hint": "spoken and translation", + "description": "Spoken, default, and translation" }, "voice": { "label": "Voice", - "hint": "text-to-speech" + "hint": "text-to-speech", + "description": "Text-to-speech voice preference" }, "dock": { "label": "Dock", - "hint": "choose and reorder" + "hint": "choose and reorder", + "description": "Choose and reorder dock items" }, "myModels": { - "label": "My Models" + "label": "My Models", + "description": "Show or hide models for yourself" }, "channelModels": { - "label": "Channel Models" + "label": "Channel Models", + "description": "Models reachable by non-owner accounts" }, "defaultModels": { - "label": "Default Models" + "label": "Default Models", + "description": "Default models for chat, projects, and tasks" }, "providers": { "label": "Providers", - "hint": "harnesses" + "hint": "harnesses", + "description": "Remote and local AI providers" }, "tts": { - "label": "Text to Speech" + "label": "Text to Speech", + "description": "TTS provider and voice" }, "stt": { - "label": "Speech to Text" + "label": "Speech to Text", + "description": "Whisper server URL" }, "ocr": { - "label": "OCR" + "label": "OCR", + "description": "Vision model for text extraction" }, "maintenance": { "label": "Rebuild & restart", - "hint": "maintenance" + "hint": "maintenance", + "description": "Load changes without a terminal" }, "smtp": { "label": "SMTP", - "hint": "outgoing mail" + "hint": "outgoing mail", + "description": "Email delivery provider" }, "googleOauth": { - "label": "Google OAuth" + "label": "Google OAuth", + "description": "Client ID and secret for Google APIs" }, "apify": { - "label": "Apify" + "label": "Apify", + "description": "API token for web scraping actors" }, "davSync": { "label": "Calendar & Contacts sync", - "hint": "app passwords" + "hint": "app passwords", + "description": "Per-device passwords for CalDAV/CardDAV clients" }, "accounts": { "label": "Accounts", - "hint": "users" + "hint": "users", + "description": "Who has access, and as what" }, "permissions": { - "label": "Permissions" + "label": "Permissions", + "description": "What each role can reach" } + }, + "searchPlaceholder": "Search settings…", + "selectSection": "Select a section", + "groups": { + "models": "Models", + "defaults": "Defaults", + "providers": "Providers", + "ai": "AI", + "maintenance": "Maintenance", + "email": "Email" + }, + "nav": { + "profile": "Profile", + "ai": "AI", + "system": "System", + "integrations": "Integrations", + "users": "Users" + }, + "ai": { + "noModels": "No models available. Configure API keys in Providers.", + "dragToShow": "Drag models here to show", + "allVisible": "All models visible", + "dragToEnable": "Drag models here to enable", + "allEnabled": "All models enabled", + "visible": "Visible", + "hidden": "Hidden", + "enabled": "Enabled", + "disabled": "Disabled", + "refresh": "Refresh models from providers", + "refreshed": "Models refreshed", + "refreshFailed": "Failed to refresh models" + }, + "integrations": { + "enterprise": "Enterprise", + "personal": "Personal" } }, "header": { diff --git a/src/apps/officer-web/locales/pt-PT.json b/src/apps/officer-web/locales/pt-PT.json index 8f4b1153..9b926e35 100644 --- a/src/apps/officer-web/locales/pt-PT.json +++ b/src/apps/officer-web/locales/pt-PT.json @@ -50,74 +50,129 @@ "sections": { "profile": { "label": "Perfil", - "hint": "avatar, nome, email" + "hint": "avatar, nome, email", + "description": "Avatar, nome e email" }, "changePassword": { - "label": "Alterar palavra-passe" + "label": "Alterar palavra-passe", + "description": "Atualize a sua palavra-passe" }, "apiKeys": { "label": "Chaves de API", - "hint": "chaves que iniciam sessão como você" + "hint": "chaves que iniciam sessão como você", + "description": "Chaves que iniciam sessão como você, revogáveis uma a uma" }, "languages": { "label": "Idiomas", - "hint": "falados e tradução" + "hint": "falados e tradução", + "description": "Falados, predefinido e tradução" }, "voice": { "label": "Voz", - "hint": "conversão de texto em fala" + "hint": "conversão de texto em fala", + "description": "Preferência de voz para conversão de texto em fala" }, "dock": { "label": "Dock", - "hint": "escolher e reordenar" + "hint": "escolher e reordenar", + "description": "Escolher e reordenar os itens do dock" }, "myModels": { - "label": "Os meus modelos" + "label": "Os meus modelos", + "description": "Mostrar ou ocultar modelos para si" }, "channelModels": { - "label": "Modelos do canal" + "label": "Modelos do canal", + "description": "Modelos acessíveis a contas que não são do proprietário" }, "defaultModels": { - "label": "Modelos predefinidos" + "label": "Modelos predefinidos", + "description": "Modelos predefinidos para chat, projetos e tarefas" }, "providers": { "label": "Fornecedores", - "hint": "harnesses" + "hint": "harnesses", + "description": "Fornecedores de IA remotos e locais" }, "tts": { - "label": "Texto para fala" + "label": "Texto para fala", + "description": "Fornecedor de TTS e voz" }, "stt": { - "label": "Fala para texto" + "label": "Fala para texto", + "description": "URL do servidor Whisper" }, "ocr": { - "label": "OCR" + "label": "OCR", + "description": "Modelo de visão para extração de texto" }, "maintenance": { "label": "Reconstruir e reiniciar", - "hint": "manutenção" + "hint": "manutenção", + "description": "Carregar alterações sem um terminal" }, "smtp": { "label": "SMTP", - "hint": "correio a enviar" + "hint": "correio a enviar", + "description": "Fornecedor de entrega de email" }, "googleOauth": { - "label": "Google OAuth" + "label": "Google OAuth", + "description": "Client ID e secret para as APIs Google" }, "apify": { - "label": "Apify" + "label": "Apify", + "description": "Token de API para atores de web scraping" }, "davSync": { "label": "Sincronização de Calendário e Contactos", - "hint": "palavras-passe de aplicação" + "hint": "palavras-passe de aplicação", + "description": "Palavras-passe por dispositivo para clientes CalDAV/CardDAV" }, "accounts": { "label": "Contas", - "hint": "utilizadores" + "hint": "utilizadores", + "description": "Quem tem acesso, e como" }, "permissions": { - "label": "Permissões" + "label": "Permissões", + "description": "O que cada função pode alcançar" } + }, + "searchPlaceholder": "Pesquisar definições…", + "selectSection": "Selecione uma secção", + "groups": { + "models": "Modelos", + "defaults": "Predefinições", + "providers": "Fornecedores", + "ai": "IA", + "maintenance": "Manutenção", + "email": "Email" + }, + "nav": { + "profile": "Perfil", + "ai": "IA", + "system": "Sistema", + "integrations": "Integrações", + "users": "Utilizadores" + }, + "ai": { + "noModels": "Nenhum modelo disponível. Configure as chaves de API em Fornecedores.", + "dragToShow": "Arraste modelos para aqui para os mostrar", + "allVisible": "Todos os modelos visíveis", + "dragToEnable": "Arraste modelos para aqui para os ativar", + "allEnabled": "Todos os modelos ativados", + "visible": "Visíveis", + "hidden": "Ocultos", + "enabled": "Ativados", + "disabled": "Desativados", + "refresh": "Atualizar modelos a partir dos fornecedores", + "refreshed": "Modelos atualizados", + "refreshFailed": "Falha ao atualizar modelos" + }, + "integrations": { + "enterprise": "Empresa", + "personal": "Pessoal" } }, "header": {