story-research-zapwall/components/Nip95ConfigManager.tsx
Nicolas Cantu 32b33d56a1 Add translations for settings page (fr/en)
**Motivations:**
- Translate settings page and all its components to French and English
- Provide consistent multilingual experience

**Root causes:**
- Settings page and components were hardcoded in English
- No translation support for key management and NIP-95 configuration

**Correctifs:**
- None (new feature)

**Evolutions:**
- Added translations for settings page title
- Added translations for KeyManagementManager component:
  - Public keys display (npub and hex)
  - Import form and validation messages
  - Recovery phrase display
  - All buttons and warnings
- Added translations for Nip95ConfigManager component:
  - Endpoint list and management
  - Add/edit/remove actions
  - Error messages
- Updated both fr.txt and en.txt translation files
- All text now uses t() function for i18n support

**Pages affectées:**
- pages/settings.tsx
- components/KeyManagementManager.tsx
- components/Nip95ConfigManager.tsx
- public/locales/fr.txt
- public/locales/en.txt
- locales/fr.txt
- locales/en.txt
2026-01-05 22:43:11 +01:00

286 lines
10 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { useState, useEffect } from 'react'
import { configStorage } from '@/lib/configStorage'
import type { Nip95Config } from '@/lib/configStorageTypes'
import { t } from '@/lib/i18n'
interface Nip95ConfigManagerProps {
onConfigChange?: () => void
}
export function Nip95ConfigManager({ onConfigChange }: Nip95ConfigManagerProps) {
const [apis, setApis] = useState<Nip95Config[]>([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
const [editingId, setEditingId] = useState<string | null>(null)
const [newUrl, setNewUrl] = useState('')
const [showAddForm, setShowAddForm] = useState(false)
useEffect(() => {
void loadApis()
}, [])
async function loadApis() {
try {
setLoading(true)
setError(null)
const config = await configStorage.getConfig()
setApis(config.nip95Apis.sort((a, b) => a.priority - b.priority))
} catch (e) {
const errorMessage = e instanceof Error ? e.message : t('settings.nip95.error.loadFailed')
setError(errorMessage)
console.error('Error loading NIP-95 APIs:', e)
} finally {
setLoading(false)
}
}
async function handleToggleEnabled(id: string, enabled: boolean) {
try {
await configStorage.updateNip95Api(id, { enabled })
await loadApis()
onConfigChange?.()
} catch (e) {
const errorMessage = e instanceof Error ? e.message : t('settings.nip95.error.updateFailed')
setError(errorMessage)
console.error('Error updating NIP-95 API:', e)
}
}
async function handleUpdatePriority(id: string, priority: number) {
try {
await configStorage.updateNip95Api(id, { priority })
await loadApis()
onConfigChange?.()
} catch (e) {
const errorMessage = e instanceof Error ? e.message : t('settings.nip95.error.priorityFailed')
setError(errorMessage)
console.error('Error updating priority:', e)
}
}
async function handleUpdateUrl(id: string, url: string) {
try {
await configStorage.updateNip95Api(id, { url })
await loadApis()
setEditingId(null)
onConfigChange?.()
} catch (e) {
const errorMessage = e instanceof Error ? e.message : t('settings.nip95.error.urlFailed')
setError(errorMessage)
console.error('Error updating URL:', e)
}
}
async function handleAddApi() {
if (!newUrl.trim()) {
setError(t('settings.nip95.error.urlRequired'))
return
}
try {
// Validate URL format
new URL(newUrl)
await configStorage.addNip95Api(newUrl.trim(), false)
setNewUrl('')
setShowAddForm(false)
await loadApis()
onConfigChange?.()
} catch (e) {
if (e instanceof TypeError && e.message.includes('Invalid URL')) {
setError(t('settings.nip95.error.invalidUrl'))
} else {
const errorMessage = e instanceof Error ? e.message : t('settings.nip95.error.addFailed')
setError(errorMessage)
}
console.error('Error adding NIP-95 API:', e)
}
}
async function handleRemoveApi(id: string) {
if (!confirm(t('settings.nip95.remove.confirm'))) {
return
}
try {
await configStorage.removeNip95Api(id)
await loadApis()
onConfigChange?.()
} catch (e) {
const errorMessage = e instanceof Error ? e.message : t('settings.nip95.error.removeFailed')
setError(errorMessage)
console.error('Error removing NIP-95 API:', e)
}
}
if (loading) {
return (
<div className="text-center py-8 text-neon-cyan">
<div>{t('settings.nip95.loading')}</div>
</div>
)
}
return (
<div className="space-y-6">
{error && (
<div className="bg-red-900/30 border border-red-500/50 rounded p-4 text-red-300">
{error}
<button
onClick={() => setError(null)}
className="ml-4 text-red-400 hover:text-red-200"
>
×
</button>
</div>
)}
<div className="flex justify-between items-center">
<h2 className="text-2xl font-bold text-neon-cyan">{t('settings.nip95.title')}</h2>
<button
onClick={() => setShowAddForm(!showAddForm)}
className="px-4 py-2 bg-neon-cyan/20 hover:bg-neon-cyan/30 text-neon-cyan border border-neon-cyan/50 rounded transition-colors"
>
{showAddForm ? t('settings.nip95.add.cancel') : t('settings.nip95.addButton')}
</button>
</div>
{showAddForm && (
<div className="bg-cyber-dark border border-neon-cyan/30 rounded p-4 space-y-4">
<div>
<label className="block text-sm font-medium text-cyber-accent mb-2">
{t('settings.nip95.add.url')}
</label>
<input
type="url"
value={newUrl}
onChange={(e) => setNewUrl(e.target.value)}
placeholder={t('settings.nip95.add.placeholder')}
className="w-full px-4 py-2 bg-cyber-darker border border-cyber-accent/30 rounded text-cyber-light focus:border-neon-cyan focus:outline-none"
/>
</div>
<div className="flex gap-2">
<button
onClick={() => void handleAddApi()}
className="px-4 py-2 bg-neon-cyan/20 hover:bg-neon-cyan/30 text-neon-cyan border border-neon-cyan/50 rounded transition-colors"
>
{t('settings.nip95.add.add')}
</button>
<button
onClick={() => {
setShowAddForm(false)
setNewUrl('')
setError(null)
}}
className="px-4 py-2 bg-cyber-accent/20 hover:bg-cyber-accent/30 text-cyber-accent border border-cyber-accent/50 rounded transition-colors"
>
{t('settings.nip95.add.cancel')}
</button>
</div>
</div>
)}
<div className="space-y-4">
{apis.length === 0 ? (
<div className="text-center py-8 text-cyber-accent">
{t('settings.nip95.empty')}
</div>
) : (
apis.map((api) => (
<div
key={api.id}
className="bg-cyber-dark border border-neon-cyan/30 rounded p-4 space-y-3"
>
<div className="flex items-start justify-between gap-4">
<div className="flex-1">
{editingId === api.id ? (
<div className="space-y-2">
<input
type="url"
defaultValue={api.url}
onBlur={(e) => {
if (e.target.value !== api.url) {
void handleUpdateUrl(api.id, e.target.value)
} else {
setEditingId(null)
}
}}
onKeyDown={(e) => {
if (e.key === 'Enter') {
e.currentTarget.blur()
} else if (e.key === 'Escape') {
setEditingId(null)
}
}}
className="w-full px-3 py-2 bg-cyber-darker border border-neon-cyan/50 rounded text-cyber-light focus:border-neon-cyan focus:outline-none"
autoFocus
/>
</div>
) : (
<div
className="text-neon-cyan cursor-pointer hover:text-neon-green transition-colors"
onClick={() => setEditingId(api.id)}
title="Click to edit URL"
>
{api.url}
</div>
)}
<div className="text-sm text-cyber-accent mt-1">
Priority: {api.priority} | ID: {api.id}
</div>
</div>
<div className="flex items-center gap-2">
<label className="flex items-center gap-2 cursor-pointer">
<input
type="checkbox"
checked={api.enabled}
onChange={(e) => void handleToggleEnabled(api.id, e.target.checked)}
className="w-4 h-4 text-neon-cyan bg-cyber-darker border-cyber-accent rounded focus:ring-neon-cyan"
/>
<span className="text-sm text-cyber-accent">
{api.enabled ? t('settings.nip95.list.enabled') : t('settings.nip95.list.disabled')}
</span>
</label>
<button
onClick={() => void handleRemoveApi(api.id)}
className="px-3 py-1 text-sm bg-red-900/30 hover:bg-red-900/50 text-red-300 border border-red-500/50 rounded transition-colors"
title={t('settings.nip95.list.remove')}
>
{t('settings.nip95.list.remove')}
</button>
</div>
</div>
<div className="flex items-center gap-4">
<label className="flex items-center gap-2">
<span className="text-sm text-cyber-accent">{t('settings.nip95.list.priority')}:</span>
<input
type="number"
min="1"
value={api.priority}
onChange={(e) => {
const priority = parseInt(e.target.value, 10)
if (!isNaN(priority) && priority > 0) {
void handleUpdatePriority(api.id, priority)
}
}}
className="w-20 px-2 py-1 bg-cyber-darker border border-cyber-accent/30 rounded text-cyber-light focus:border-neon-cyan focus:outline-none"
/>
</label>
</div>
</div>
))
)}
</div>
<div className="text-sm text-cyber-accent space-y-2">
<p>
<strong>Note:</strong> Endpoints are tried in priority order (lower number = higher priority).
Only enabled endpoints will be used for uploads.
</p>
<p>
If an endpoint fails, the next enabled endpoint will be tried automatically.
</p>
</div>
</div>
)
}