feat: add password confirmation to register + fix i18n hardcoded strings

- Add confirmPassword field to registration form with Zod validation
- Replace ~30 hardcoded French/English strings in admin-settings-form
  with proper t() i18n calls (Ollama models, Custom models, search test)
- Extract SettingsHeader to client component for i18n support
- Add 15 i18n keys to all 15 locale files (auth + admin.ai + admin.tools)
- Remove debug "Config value" line from embeddings section

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-04-26 19:59:38 +02:00
parent bbaae76103
commit e358171c45
20 changed files with 385 additions and 114 deletions

View File

@@ -91,11 +91,11 @@ export function AdminSettingsForm({ config }: { config: Record<string, string> }
else if (type === 'embeddings') setOllamaEmbeddingsModels(result.models)
else setOllamaChatModels(result.models)
} else {
toast.error(`Failed to fetch Ollama models: ${result.error}`)
toast.error(`${t('admin.ai.fetchModelsFailed')}: ${result.error}`)
}
} catch (error) {
console.error(error)
toast.error('Failed to fetch Ollama models')
toast.error(t('admin.ai.fetchModelsFailed'))
} finally {
if (type === 'tags') setIsLoadingTagsModels(false)
else if (type === 'embeddings') setIsLoadingEmbeddingsModels(false)
@@ -122,11 +122,11 @@ export function AdminSettingsForm({ config }: { config: Record<string, string> }
else if (type === 'embeddings') setCustomEmbeddingsModels(result.models)
else setCustomChatModels(result.models)
} else {
toast.error(`Impossible de récupérer les modèles : ${result.error}`)
toast.error(`${t('admin.ai.fetchModelsFailed')}: ${result.error}`)
}
} catch (error) {
console.error(error)
toast.error('Erreur lors de la récupération des modèles')
toast.error(t('admin.ai.fetchModelsFailed'))
} finally {
if (type === 'tags') setIsLoadingCustomTagsModels(false)
else if (type === 'embeddings') setIsLoadingCustomEmbeddingsModels(false)
@@ -439,7 +439,7 @@ export function AdminSettingsForm({ config }: { config: Record<string, string> }
fetchOllamaModels('tags', input.value)
}}
disabled={isLoadingTagsModels}
title="Refresh Models"
title={t('admin.ai.refreshModels')}
>
<RefreshCw className={`h-4 w-4 ${isLoadingTagsModels ? 'animate-spin' : ''}`} />
</Button>
@@ -455,15 +455,20 @@ export function AdminSettingsForm({ config }: { config: Record<string, string> }
className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
>
{ollamaTagsModels.length > 0 ? (
ollamaTagsModels.map((model) => (
<option key={model} value={model}>{model}</option>
))
<>
{!ollamaTagsModels.includes(selectedTagsModel) && selectedTagsModel && (
<option value={selectedTagsModel}>{selectedTagsModel} ({t('admin.ai.configured')})</option>
)}
{ollamaTagsModels.map((model) => (
<option key={model} value={model}>{model}</option>
))}
</>
) : (
<option value={selectedTagsModel || 'granite4:latest'}>{selectedTagsModel || 'granite4:latest'} {t('admin.ai.saved')}</option>
)}
</select>
<p className="text-xs text-muted-foreground">
{isLoadingTagsModels ? 'Fetching models...' : t('admin.ai.selectOllamaModel')}
{isLoadingTagsModels ? t('admin.ai.fetchingModels') : t('admin.ai.selectOllamaModel')}
</p>
</div>
</div>
@@ -515,7 +520,7 @@ export function AdminSettingsForm({ config }: { config: Record<string, string> }
fetchCustomModels('tags', urlInput.value, keyInput.value)
}}
disabled={isLoadingCustomTagsModels}
title="Récupérer les modèles"
title={t('admin.ai.refreshModels')}
>
<RefreshCw className={`h-4 w-4 ${isLoadingCustomTagsModels ? 'animate-spin' : ''}`} />
</Button>
@@ -537,16 +542,16 @@ export function AdminSettingsForm({ config }: { config: Record<string, string> }
}
value={selectedTagsModel}
onChange={setSelectedTagsModel}
placeholder={selectedTagsModel || 'Cliquez sur ↺ pour charger les modèles'}
searchPlaceholder="Rechercher un modèle..."
emptyMessage="Aucun modèle. Cliquez sur ↺"
placeholder={selectedTagsModel || t('admin.ai.clickToLoadModels')}
searchPlaceholder={t('admin.ai.searchModel')}
emptyMessage={t('admin.ai.noModels')}
/>
<p className="text-xs text-muted-foreground">
{isLoadingCustomTagsModels
? 'Récupération des modèles...'
? t('admin.ai.fetchingModels')
: customTagsModels.length > 0
? `${customTagsModels.length} modèle(s) disponible(s)`
: 'Renseignez l\'URL et cliquez sur ↺ pour charger les modèles'}
? t('admin.ai.modelsAvailable', { count: customTagsModels.length })
: t('admin.ai.enterUrlToLoad')}
</p>
</div>
</div>
@@ -563,7 +568,7 @@ export function AdminSettingsForm({ config }: { config: Record<string, string> }
<Label htmlFor="AI_PROVIDER_EMBEDDING">
{t('admin.ai.provider')}
<span className="ml-2 text-xs text-muted-foreground">
(Current: {embeddingsProvider})
{t('admin.ai.currentProvider', { provider: embeddingsProvider })}
</span>
</Label>
<select
@@ -586,9 +591,6 @@ export function AdminSettingsForm({ config }: { config: Record<string, string> }
<option value="openai">{t('admin.ai.providerOpenAIOption')}</option>
<option value="custom">{t('admin.ai.providerCustomOption')}</option>
</select>
<p className="text-xs text-muted-foreground">
Config value: {config.AI_PROVIDER_EMBEDDING || 'Not set (defaults to ollama)'}
</p>
</div>
{embeddingsProvider === 'ollama' && (
@@ -611,7 +613,7 @@ export function AdminSettingsForm({ config }: { config: Record<string, string> }
fetchOllamaModels('embeddings', input.value)
}}
disabled={isLoadingEmbeddingsModels}
title="Refresh Models"
title={t('admin.ai.refreshModels')}
>
<RefreshCw className={`h-4 w-4 ${isLoadingEmbeddingsModels ? 'animate-spin' : ''}`} />
</Button>
@@ -627,15 +629,20 @@ export function AdminSettingsForm({ config }: { config: Record<string, string> }
className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
>
{ollamaEmbeddingsModels.length > 0 ? (
ollamaEmbeddingsModels.map((model) => (
<option key={model} value={model}>{model}</option>
))
<>
{!ollamaEmbeddingsModels.includes(selectedEmbeddingModel) && selectedEmbeddingModel && (
<option value={selectedEmbeddingModel}>{selectedEmbeddingModel} ({t('admin.ai.configured')})</option>
)}
{ollamaEmbeddingsModels.map((model) => (
<option key={model} value={model}>{model}</option>
))}
</>
) : (
<option value={selectedEmbeddingModel || 'embeddinggemma:latest'}>{selectedEmbeddingModel || 'embeddinggemma:latest'} {t('admin.ai.saved')}</option>
)}
</select>
<p className="text-xs text-muted-foreground">
{isLoadingEmbeddingsModels ? 'Fetching models...' : t('admin.ai.selectEmbeddingModel')}
{isLoadingEmbeddingsModels ? t('admin.ai.fetchingModels') : t('admin.ai.selectEmbeddingModel')}
</p>
</div>
</div>
@@ -687,7 +694,7 @@ export function AdminSettingsForm({ config }: { config: Record<string, string> }
fetchCustomModels('embeddings', urlInput.value, keyInput.value)
}}
disabled={isLoadingCustomEmbeddingsModels}
title="Récupérer les modèles"
title={t('admin.ai.refreshModels')}
>
<RefreshCw className={`h-4 w-4 ${isLoadingCustomEmbeddingsModels ? 'animate-spin' : ''}`} />
</Button>
@@ -709,16 +716,16 @@ export function AdminSettingsForm({ config }: { config: Record<string, string> }
}
value={selectedEmbeddingModel}
onChange={setSelectedEmbeddingModel}
placeholder={selectedEmbeddingModel || 'Cliquez sur ↺ pour charger les modèles'}
searchPlaceholder="Rechercher un modèle..."
emptyMessage="Aucun modèle. Cliquez sur ↺"
placeholder={selectedEmbeddingModel || t('admin.ai.clickToLoadModels')}
searchPlaceholder={t('admin.ai.searchModel')}
emptyMessage={t('admin.ai.noModels')}
/>
<p className="text-xs text-muted-foreground">
{isLoadingCustomEmbeddingsModels
? 'Récupération des modèles...'
? t('admin.ai.fetchingModels')
: customEmbeddingsModels.length > 0
? `${customEmbeddingsModels.length} modèle(s) disponible(s)`
: 'Renseignez l\'URL et cliquez sur ↺ pour charger les modèles'}
? t('admin.ai.modelsAvailable', { count: customEmbeddingsModels.length })
: t('admin.ai.enterUrlToLoad')}
</p>
</div>
</div>
@@ -776,7 +783,7 @@ export function AdminSettingsForm({ config }: { config: Record<string, string> }
fetchOllamaModels('chat', input.value)
}}
disabled={isLoadingChatModels}
title="Refresh Models"
title={t('admin.ai.refreshModels')}
>
<RefreshCw className={`h-4 w-4 ${isLoadingChatModels ? 'animate-spin' : ''}`} />
</Button>
@@ -792,15 +799,20 @@ export function AdminSettingsForm({ config }: { config: Record<string, string> }
className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
>
{ollamaChatModels.length > 0 ? (
ollamaChatModels.map((model) => (
<option key={model} value={model}>{model}</option>
))
<>
{!ollamaChatModels.includes(selectedChatModel) && selectedChatModel && (
<option value={selectedChatModel}>{selectedChatModel} ({t('admin.ai.configured')})</option>
)}
{ollamaChatModels.map((model) => (
<option key={model} value={model}>{model}</option>
))}
</>
) : (
<option value={selectedChatModel || 'granite4:latest'}>{selectedChatModel || 'granite4:latest'} {t('admin.ai.saved')}</option>
)}
</select>
<p className="text-xs text-muted-foreground">
{isLoadingChatModels ? 'Fetching models...' : t('admin.ai.selectOllamaModel')}
{isLoadingChatModels ? t('admin.ai.fetchingModels') : t('admin.ai.selectOllamaModel')}
</p>
</div>
</div>
@@ -852,7 +864,7 @@ export function AdminSettingsForm({ config }: { config: Record<string, string> }
fetchCustomModels('chat', urlInput.value, keyInput.value)
}}
disabled={isLoadingCustomChatModels}
title="Récupérer les modèles"
title={t('admin.ai.refreshModels')}
>
<RefreshCw className={`h-4 w-4 ${isLoadingCustomChatModels ? 'animate-spin' : ''}`} />
</Button>
@@ -874,16 +886,16 @@ export function AdminSettingsForm({ config }: { config: Record<string, string> }
}
value={selectedChatModel}
onChange={setSelectedChatModel}
placeholder={selectedChatModel || 'Cliquez sur ↺ pour charger les modèles'}
searchPlaceholder="Rechercher un modèle..."
emptyMessage="Aucun modèle. Cliquez sur ↺"
placeholder={selectedChatModel || t('admin.ai.clickToLoadModels')}
searchPlaceholder={t('admin.ai.searchModel')}
emptyMessage={t('admin.ai.noModels')}
/>
<p className="text-xs text-muted-foreground">
{isLoadingCustomChatModels
? 'Récupération des modèles...'
? t('admin.ai.fetchingModels')
: customChatModels.length > 0
? `${customChatModels.length} modèle(s) disponible(s)`
: 'Renseignez l\'URL et cliquez sur ↺ pour charger les modèles'}
? t('admin.ai.modelsAvailable', { count: customChatModels.length })
: t('admin.ai.enterUrlToLoad')}
</p>
</div>
</div>
@@ -1105,7 +1117,7 @@ export function AdminSettingsForm({ config }: { config: Record<string, string> }
onClick={handleTestSearch}
disabled={isTestingSearch}
>
{isTestingSearch ? 'Test en cours…' : 'Tester la recherche web'}
{isTestingSearch ? t('admin.tools.testing') : t('admin.tools.testSearch')}
</Button>
</CardFooter>
</form>