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
This commit is contained in:
Nicolas Cantu 2026-01-05 22:43:11 +01:00
parent d7a04dd8f8
commit 32b33d56a1
7 changed files with 347 additions and 70 deletions

View File

@ -2,6 +2,7 @@ import { useState, useEffect } from 'react'
import { nostrAuthService } from '@/lib/nostrAuth'
import { keyManagementService } from '@/lib/keyManagement'
import { nip19 } from 'nostr-tools'
import { t } from '@/lib/i18n'
interface PublicKeys {
publicKey: string
@ -41,7 +42,7 @@ export function KeyManagementManager() {
}
}
} catch (e) {
const errorMessage = e instanceof Error ? e.message : 'Failed to load keys'
const errorMessage = e instanceof Error ? e.message : t('settings.keyManagement.loading')
setError(errorMessage)
console.error('Error loading keys:', e)
} finally {
@ -79,14 +80,14 @@ export function KeyManagementManager() {
async function handleImport() {
if (!importKey.trim()) {
setError('Please enter a private key')
setError(t('settings.keyManagement.import.error.required'))
return
}
// Extract key from URL or text
const extractedKey = extractKeyFromUrl(importKey.trim())
if (!extractedKey) {
setError('Invalid key format. Please provide a nsec (nsec1...) or hex private key.')
setError(t('settings.keyManagement.import.error.invalid'))
return
}
@ -104,8 +105,7 @@ export function KeyManagementManager() {
} catch (e) {
// If decoding failed, assume it's hex, validate length (64 hex chars = 32 bytes)
if (!/^[0-9a-f]{64}$/i.test(extractedKey)) {
const errorMsg = e instanceof Error ? e.message : 'Invalid format'
setError(`Invalid key format: ${errorMsg}. Please provide a nsec (nsec1...) or hex (64 characters) private key.`)
setError(t('settings.keyManagement.import.error.invalid'))
return
}
}
@ -138,7 +138,7 @@ export function KeyManagementManager() {
setShowImportForm(false)
await loadKeys()
} catch (e) {
const errorMessage = e instanceof Error ? e.message : 'Failed to import key'
const errorMessage = e instanceof Error ? e.message : t('settings.keyManagement.import.error.failed')
setError(errorMessage)
console.error('Error importing key:', e)
} finally {
@ -194,7 +194,7 @@ export function KeyManagementManager() {
if (loading) {
return (
<div className="bg-cyber-darker border border-neon-cyan/30 rounded-lg p-6">
<p className="text-cyber-accent">Loading...</p>
<p className="text-cyber-accent">{t('settings.keyManagement.loading')}</p>
</div>
)
}
@ -202,7 +202,7 @@ export function KeyManagementManager() {
return (
<div className="space-y-6">
<div className="bg-cyber-darker border border-neon-cyan/30 rounded-lg p-6">
<h2 className="text-2xl font-bold text-neon-cyan mb-4">Key Management</h2>
<h2 className="text-2xl font-bold text-neon-cyan mb-4">{t('settings.keyManagement.title')}</h2>
{error && (
<div className="bg-red-900/20 border border-red-400/50 rounded-lg p-4 mb-4">
@ -215,14 +215,14 @@ export function KeyManagementManager() {
<div className="space-y-4 mb-6">
<div className="bg-neon-blue/10 border border-neon-blue/30 rounded-lg p-4">
<div className="flex justify-between items-start mb-2">
<p className="text-neon-blue font-semibold">Public Key (npub)</p>
<p className="text-neon-blue font-semibold">{t('settings.keyManagement.publicKey.npub')}</p>
<button
onClick={() => {
void handleCopyNpub()
}}
className="px-3 py-1 text-xs bg-cyber-light border border-neon-cyan/30 hover:border-neon-cyan/50 hover:bg-cyber-dark text-cyber-accent hover:text-neon-cyan rounded transition-colors"
>
{copiedNpub ? '✓ Copied' : 'Copy'}
{copiedNpub ? t('settings.keyManagement.copied') : t('settings.keyManagement.copy')}
</button>
</div>
<p className="text-neon-cyan text-sm font-mono break-all">{publicKeys.npub}</p>
@ -230,14 +230,14 @@ export function KeyManagementManager() {
<div className="bg-neon-blue/10 border border-neon-blue/30 rounded-lg p-4">
<div className="flex justify-between items-start mb-2">
<p className="text-neon-blue font-semibold">Public Key (hex)</p>
<p className="text-neon-blue font-semibold">{t('settings.keyManagement.publicKey.hex')}</p>
<button
onClick={() => {
void handleCopyPublicKey()
}}
className="px-3 py-1 text-xs bg-cyber-light border border-neon-cyan/30 hover:border-neon-cyan/50 hover:bg-cyber-dark text-cyber-accent hover:text-neon-cyan rounded transition-colors"
>
{copiedPublicKey ? '✓ Copied' : 'Copy'}
{copiedPublicKey ? t('settings.keyManagement.copied') : t('settings.keyManagement.copy')}
</button>
</div>
<p className="text-neon-cyan text-sm font-mono break-all">{publicKeys.publicKey}</p>
@ -247,9 +247,9 @@ export function KeyManagementManager() {
{!publicKeys && !accountExists && (
<div className="bg-yellow-900/20 border border-yellow-400/50 rounded-lg p-4 mb-6">
<p className="text-yellow-400 font-semibold mb-2">No account found</p>
<p className="text-yellow-400 font-semibold mb-2">{t('settings.keyManagement.noAccount.title')}</p>
<p className="text-yellow-300/90 text-sm">
Create a new account by importing a private key. The key will be encrypted using a two-level encryption system.
{t('settings.keyManagement.noAccount.description')}
</p>
</div>
)}
@ -263,28 +263,23 @@ export function KeyManagementManager() {
}}
className="w-full py-3 px-6 bg-neon-cyan/20 hover:bg-neon-cyan/30 text-neon-cyan rounded-lg font-medium transition-all border border-neon-cyan/50 hover:shadow-glow-cyan"
>
{accountExists ? 'Replace Account (Import New Key)' : 'Import Private Key'}
{accountExists ? t('settings.keyManagement.import.button.replace') : t('settings.keyManagement.import.button.new')}
</button>
)}
{showImportForm && (
<div className="space-y-4">
<div className="bg-yellow-900/20 border border-yellow-400/50 rounded-lg p-4">
<p className="text-yellow-400 font-semibold mb-2"> Important</p>
<p className="text-yellow-300/90 text-sm">
After importing, you will receive <strong className="font-bold">4 recovery words</strong> (BIP39 dictionary) to secure your account.
These words encrypt a Key Encryption Key (KEK) stored in the browser&apos;s Credentials API, which then encrypts your private key stored in IndexedDB (two-level encryption system).
</p>
<p className="text-yellow-400 font-semibold mb-2">{t('settings.keyManagement.import.warning.title')}</p>
<p className="text-yellow-300/90 text-sm" dangerouslySetInnerHTML={{ __html: t('settings.keyManagement.import.warning.description') }} />
{accountExists && (
<p className="text-yellow-300/90 text-sm mt-2">
<strong className="font-bold">Warning:</strong> Importing a new key will replace your existing account. Make sure you have your recovery phrase saved before proceeding.
</p>
<p className="text-yellow-300/90 text-sm mt-2" dangerouslySetInnerHTML={{ __html: t('settings.keyManagement.import.warning.replace') }} />
)}
</div>
<div>
<label htmlFor="importKey" className="block text-sm font-medium text-cyber-accent mb-2">
Private Key (nsec URL, nsec1..., or hex)
{t('settings.keyManagement.import.label')}
</label>
<textarea
id="importKey"
@ -293,20 +288,20 @@ export function KeyManagementManager() {
setImportKey(e.target.value)
setError(null)
}}
placeholder="nsec1... or nostr://nsec1... or hex key"
placeholder={t('settings.keyManagement.import.placeholder')}
className="w-full px-3 py-2 bg-cyber-dark border border-neon-cyan/30 rounded-lg font-mono text-sm text-neon-cyan focus:border-neon-cyan focus:outline-none"
rows={4}
/>
<p className="text-sm text-cyber-accent/70 mt-2">
You can paste a nsec key, a nostr:// URL containing a nsec, or a hex private key (64 characters).
{t('settings.keyManagement.import.help')}
</p>
</div>
{showReplaceWarning && (
<div className="bg-red-900/20 border border-red-400/50 rounded-lg p-4">
<p className="text-red-400 font-semibold mb-2"> Replace Existing Account?</p>
<p className="text-red-400 font-semibold mb-2">{t('settings.keyManagement.replace.warning.title')}</p>
<p className="text-red-300/90 text-sm mb-4">
This will delete your current account and create a new one with the imported key. Make sure you have saved your recovery phrase for the current account.
{t('settings.keyManagement.replace.warning.description')}
</p>
<div className="flex gap-4">
<button
@ -315,7 +310,7 @@ export function KeyManagementManager() {
}}
className="flex-1 py-2 px-4 bg-cyber-light border border-neon-cyan/30 hover:border-neon-cyan/50 hover:bg-cyber-dark text-cyber-accent hover:text-neon-cyan rounded-lg font-medium transition-colors"
>
Cancel
{t('settings.keyManagement.replace.cancel')}
</button>
<button
onClick={() => {
@ -324,7 +319,7 @@ export function KeyManagementManager() {
disabled={importing}
className="flex-1 py-2 px-4 bg-red-600/20 hover:bg-red-600/30 text-red-400 rounded-lg font-medium transition-all border border-red-400/50 hover:shadow-glow-red disabled:opacity-50"
>
{importing ? 'Replacing...' : 'Replace Account'}
{importing ? t('settings.keyManagement.replace.replacing') : t('settings.keyManagement.replace.confirm')}
</button>
</div>
</div>
@ -340,7 +335,7 @@ export function KeyManagementManager() {
}}
className="flex-1 py-2 px-4 bg-cyber-light border border-neon-cyan/30 hover:border-neon-cyan/50 hover:bg-cyber-dark text-cyber-accent hover:text-neon-cyan rounded-lg font-medium transition-colors"
>
Cancel
{t('settings.keyManagement.import.cancel')}
</button>
<button
onClick={() => {
@ -349,7 +344,7 @@ export function KeyManagementManager() {
disabled={importing}
className="flex-1 py-2 px-4 bg-neon-cyan/20 hover:bg-neon-cyan/30 text-neon-cyan rounded-lg font-medium transition-all border border-neon-cyan/50 hover:shadow-glow-cyan disabled:opacity-50"
>
{importing ? 'Importing...' : 'Import'}
{importing ? t('settings.keyManagement.import.importing') : t('settings.keyManagement.import.import')}
</button>
</div>
)}
@ -360,16 +355,11 @@ export function KeyManagementManager() {
{recoveryPhrase && newNpub && (
<div className="mt-6 space-y-4">
<div className="bg-yellow-900/20 border border-yellow-400/50 rounded-lg p-4">
<p className="text-yellow-400 font-semibold mb-2"> Important</p>
<p className="text-yellow-300/90 text-sm">
These <strong className="font-bold">4 recovery words</strong> are your only way to recover your account.
<strong className="font-bold"> They will never be displayed again.</strong>
</p>
<p className="text-yellow-400 font-semibold mb-2">{t('settings.keyManagement.recovery.warning.title')}</p>
<p className="text-yellow-300/90 text-sm" dangerouslySetInnerHTML={{ __html: t('settings.keyManagement.recovery.warning.part1') }} />
<p className="text-yellow-300/90 text-sm mt-2" dangerouslySetInnerHTML={{ __html: t('settings.keyManagement.recovery.warning.part2') }} />
<p className="text-yellow-300/90 text-sm mt-2">
These words (BIP39 dictionary) are used with <strong>PBKDF2</strong> to encrypt a Key Encryption Key (KEK) stored in the browser&apos;s Credentials API. This KEK then encrypts your private key stored in IndexedDB (two-level system).
</p>
<p className="text-yellow-300/90 text-sm mt-2">
Save them in a safe place. Without these words, you will permanently lose access to your account.
{t('settings.keyManagement.recovery.warning.part3')}
</p>
</div>
@ -391,12 +381,12 @@ export function KeyManagementManager() {
}}
className="w-full py-2 px-4 bg-cyber-light border border-neon-cyan/30 hover:border-neon-cyan/50 hover:bg-cyber-dark text-cyber-accent hover:text-neon-cyan rounded-lg text-sm font-medium transition-colors"
>
{copiedRecoveryPhrase ? '✓ Copied!' : 'Copy Recovery Words'}
{copiedRecoveryPhrase ? t('settings.keyManagement.recovery.copied') : t('settings.keyManagement.recovery.copy')}
</button>
</div>
<div className="bg-neon-blue/10 border border-neon-blue/30 rounded-lg p-4">
<p className="text-neon-blue font-semibold mb-2">Your new public key (npub)</p>
<p className="text-neon-blue font-semibold mb-2">{t('settings.keyManagement.recovery.newNpub')}</p>
<p className="text-neon-cyan text-sm font-mono break-all">{newNpub}</p>
</div>
@ -408,7 +398,7 @@ export function KeyManagementManager() {
}}
className="w-full py-2 px-4 bg-neon-cyan/20 hover:bg-neon-cyan/30 text-neon-cyan rounded-lg font-medium transition-all border border-neon-cyan/50 hover:shadow-glow-cyan"
>
Done
{t('settings.keyManagement.recovery.done')}
</button>
</div>
)}

View File

@ -1,6 +1,7 @@
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
@ -25,7 +26,7 @@ export function Nip95ConfigManager({ onConfigChange }: Nip95ConfigManagerProps)
const config = await configStorage.getConfig()
setApis(config.nip95Apis.sort((a, b) => a.priority - b.priority))
} catch (e) {
const errorMessage = e instanceof Error ? e.message : 'Failed to load NIP-95 APIs'
const errorMessage = e instanceof Error ? e.message : t('settings.nip95.error.loadFailed')
setError(errorMessage)
console.error('Error loading NIP-95 APIs:', e)
} finally {
@ -39,7 +40,7 @@ export function Nip95ConfigManager({ onConfigChange }: Nip95ConfigManagerProps)
await loadApis()
onConfigChange?.()
} catch (e) {
const errorMessage = e instanceof Error ? e.message : 'Failed to update API'
const errorMessage = e instanceof Error ? e.message : t('settings.nip95.error.updateFailed')
setError(errorMessage)
console.error('Error updating NIP-95 API:', e)
}
@ -51,7 +52,7 @@ export function Nip95ConfigManager({ onConfigChange }: Nip95ConfigManagerProps)
await loadApis()
onConfigChange?.()
} catch (e) {
const errorMessage = e instanceof Error ? e.message : 'Failed to update priority'
const errorMessage = e instanceof Error ? e.message : t('settings.nip95.error.priorityFailed')
setError(errorMessage)
console.error('Error updating priority:', e)
}
@ -64,7 +65,7 @@ export function Nip95ConfigManager({ onConfigChange }: Nip95ConfigManagerProps)
setEditingId(null)
onConfigChange?.()
} catch (e) {
const errorMessage = e instanceof Error ? e.message : 'Failed to update URL'
const errorMessage = e instanceof Error ? e.message : t('settings.nip95.error.urlFailed')
setError(errorMessage)
console.error('Error updating URL:', e)
}
@ -72,7 +73,7 @@ export function Nip95ConfigManager({ onConfigChange }: Nip95ConfigManagerProps)
async function handleAddApi() {
if (!newUrl.trim()) {
setError('URL is required')
setError(t('settings.nip95.error.urlRequired'))
return
}
@ -86,9 +87,9 @@ export function Nip95ConfigManager({ onConfigChange }: Nip95ConfigManagerProps)
onConfigChange?.()
} catch (e) {
if (e instanceof TypeError && e.message.includes('Invalid URL')) {
setError('Invalid URL format')
setError(t('settings.nip95.error.invalidUrl'))
} else {
const errorMessage = e instanceof Error ? e.message : 'Failed to add API'
const errorMessage = e instanceof Error ? e.message : t('settings.nip95.error.addFailed')
setError(errorMessage)
}
console.error('Error adding NIP-95 API:', e)
@ -96,7 +97,7 @@ export function Nip95ConfigManager({ onConfigChange }: Nip95ConfigManagerProps)
}
async function handleRemoveApi(id: string) {
if (!confirm('Are you sure you want to remove this endpoint?')) {
if (!confirm(t('settings.nip95.remove.confirm'))) {
return
}
@ -105,7 +106,7 @@ export function Nip95ConfigManager({ onConfigChange }: Nip95ConfigManagerProps)
await loadApis()
onConfigChange?.()
} catch (e) {
const errorMessage = e instanceof Error ? e.message : 'Failed to remove API'
const errorMessage = e instanceof Error ? e.message : t('settings.nip95.error.removeFailed')
setError(errorMessage)
console.error('Error removing NIP-95 API:', e)
}
@ -114,7 +115,7 @@ export function Nip95ConfigManager({ onConfigChange }: Nip95ConfigManagerProps)
if (loading) {
return (
<div className="text-center py-8 text-neon-cyan">
<div>Loading...</div>
<div>{t('settings.nip95.loading')}</div>
</div>
)
}
@ -134,12 +135,12 @@ export function Nip95ConfigManager({ onConfigChange }: Nip95ConfigManagerProps)
)}
<div className="flex justify-between items-center">
<h2 className="text-2xl font-bold text-neon-cyan">NIP-95 Upload Endpoints</h2>
<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 ? 'Cancel' : '+ Add Endpoint'}
{showAddForm ? t('settings.nip95.add.cancel') : t('settings.nip95.addButton')}
</button>
</div>
@ -147,13 +148,13 @@ export function Nip95ConfigManager({ onConfigChange }: Nip95ConfigManagerProps)
<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">
Endpoint URL
{t('settings.nip95.add.url')}
</label>
<input
type="url"
value={newUrl}
onChange={(e) => setNewUrl(e.target.value)}
placeholder="https://example.com/upload"
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>
@ -162,7 +163,7 @@ export function Nip95ConfigManager({ onConfigChange }: Nip95ConfigManagerProps)
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"
>
Add
{t('settings.nip95.add.add')}
</button>
<button
onClick={() => {
@ -172,7 +173,7 @@ export function Nip95ConfigManager({ onConfigChange }: Nip95ConfigManagerProps)
}}
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"
>
Cancel
{t('settings.nip95.add.cancel')}
</button>
</div>
</div>
@ -181,7 +182,7 @@ export function Nip95ConfigManager({ onConfigChange }: Nip95ConfigManagerProps)
<div className="space-y-4">
{apis.length === 0 ? (
<div className="text-center py-8 text-cyber-accent">
No NIP-95 endpoints configured
{t('settings.nip95.empty')}
</div>
) : (
apis.map((api) => (
@ -236,21 +237,21 @@ export function Nip95ConfigManager({ onConfigChange }: Nip95ConfigManagerProps)
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 ? 'Enabled' : 'Disabled'}
{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="Remove endpoint"
title={t('settings.nip95.list.remove')}
>
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">Priority:</span>
<span className="text-sm text-cyber-accent">{t('settings.nip95.list.priority')}:</span>
<input
type="number"
min="1"

View File

@ -85,7 +85,7 @@ presentation.fallback.user=User
# Filters
filters.clear=Clear all
filters.author=By author
filters.author=All authors
filters.sort=Sort by
filters.sort.newest=Newest
filters.sort.oldest=Oldest
@ -103,3 +103,69 @@ footer.privacy=Privacy Policy
common.loading=Loading...
common.error=Error
common.back=Back
common.open=Open
# Settings
settings.title=Settings
settings.keyManagement.title=Key Management
settings.keyManagement.loading=Loading...
settings.keyManagement.publicKey.npub=Public Key (npub)
settings.keyManagement.publicKey.hex=Public Key (hex)
settings.keyManagement.copy=Copy
settings.keyManagement.copied=✓ Copied
settings.keyManagement.noAccount.title=No account found
settings.keyManagement.noAccount.description=Create a new account by importing a private key. The key will be encrypted using a two-level encryption system.
settings.keyManagement.import.button.new=Import Private Key
settings.keyManagement.import.button.replace=Replace Account (Import New Key)
settings.keyManagement.import.warning.title=⚠️ Important
settings.keyManagement.import.warning.description=After importing, you will receive <strong>4 recovery words</strong> (BIP39 dictionary) to secure your account. These words encrypt a Key Encryption Key (KEK) stored in the browser's Credentials API, which then encrypts your private key stored in IndexedDB (two-level encryption system).
settings.keyManagement.import.warning.replace=Warning: Importing a new key will replace your existing account. Make sure you have your recovery phrase saved before proceeding.
settings.keyManagement.import.label=Private Key (nsec URL, nsec1..., or hex)
settings.keyManagement.import.placeholder=nsec1... or nostr://nsec1... or hex key
settings.keyManagement.import.help=You can paste a nsec key, a nostr:// URL containing a nsec, or a hex private key (64 characters).
settings.keyManagement.import.error.required=Please enter a private key
settings.keyManagement.import.error.invalid=Invalid key format. Please provide a nsec (nsec1...) or hex (64 characters) private key.
settings.keyManagement.import.error.failed=Failed to import key
settings.keyManagement.replace.warning.title=⚠️ Replace Existing Account?
settings.keyManagement.replace.warning.description=This will delete your current account and create a new one with the imported key. Make sure you have saved your recovery phrase for the current account.
settings.keyManagement.replace.cancel=Cancel
settings.keyManagement.replace.confirm=Replace Account
settings.keyManagement.replace.replacing=Replacing...
settings.keyManagement.import.cancel=Cancel
settings.keyManagement.import.importing=Importing...
settings.keyManagement.import.import=Import
settings.keyManagement.recovery.warning.title=⚠️ Important
settings.keyManagement.recovery.warning.part1=These <strong>4 recovery words</strong> are your only way to recover your account. <strong>They will never be displayed again.</strong>
settings.keyManagement.recovery.warning.part2=These words (BIP39 dictionary) are used with <strong>PBKDF2</strong> to encrypt a Key Encryption Key (KEK) stored in the browser's Credentials API. This KEK then encrypts your private key stored in IndexedDB (two-level system).
settings.keyManagement.recovery.warning.part3=Save them in a safe place. Without these words, you will permanently lose access to your account.
settings.keyManagement.recovery.copy=Copy Recovery Words
settings.keyManagement.recovery.copied=✓ Copied!
settings.keyManagement.recovery.newNpub=Your new public key (npub)
settings.keyManagement.recovery.done=Done
settings.nip95.title=NIP-95 Upload Endpoints
settings.nip95.loading=Loading...
settings.nip95.error.loadFailed=Failed to load NIP-95 APIs
settings.nip95.error.updateFailed=Failed to update API
settings.nip95.error.priorityFailed=Failed to update priority
settings.nip95.error.urlFailed=Failed to update URL
settings.nip95.error.addFailed=Failed to add API
settings.nip95.error.removeFailed=Failed to remove API
settings.nip95.error.invalidUrl=Invalid URL format
settings.nip95.error.urlRequired=URL is required
settings.nip95.addButton=Add endpoint
settings.nip95.add.url=Endpoint URL
settings.nip95.add.placeholder=https://example.com/api/upload
settings.nip95.add.add=Add
settings.nip95.add.cancel=Cancel
settings.nip95.add.adding=Adding...
settings.nip95.list.enabled=Enabled
settings.nip95.list.disabled=Disabled
settings.nip95.list.priority=Priority
settings.nip95.list.url=URL
settings.nip95.list.actions=Actions
settings.nip95.list.edit=Edit
settings.nip95.list.save=Save
settings.nip95.list.cancel=Cancel
settings.nip95.list.remove=Remove
settings.nip95.remove.confirm=Are you sure you want to remove this endpoint?
settings.nip95.empty=No endpoints configured

View File

@ -57,13 +57,35 @@ publish.publishing=Publication...
# Presentation
presentation.title=Créer votre article de présentation
presentation.description=Cet article est obligatoire pour publier sur zapwall.fr. Il permet aux lecteurs de vous connaître et de vous sponsoriser.
presentation.profileNote=Les données de ce profil sont spécifiques à zapwall.fr et peuvent différer de votre profil Nostr.
presentation.success=Article de présentation créé !
presentation.successMessage=Votre article de présentation a été créé avec succès. Vous pouvez maintenant publier des articles.
presentation.profileNote=Les données de ce profil sont spécifiques à zapwall.fr et peuvent différer de votre profil Nostr.
presentation.field.picture=Photo de profil
presentation.field.picture.help=Image de profil pour votre page auteur (max 5Mo, formats: PNG, JPG, WebP)
presentation.field.picture.change=Changer l'image
presentation.field.picture.upload=Télécharger une image
presentation.field.picture.uploading=Upload en cours...
presentation.field.picture.remove=Supprimer
presentation.field.picture.error.imagesOnly=Seules les images sont autorisées
presentation.field.picture.error.uploadFailed=Erreur lors de l'upload
presentation.field.authorName=Nom d'auteur
presentation.field.authorName.placeholder=Votre nom d'auteur
presentation.field.authorName.help=Ce nom sera affiché à la place de votre clé publique sur votre profil
presentation.field.presentation=Présentation personnelle
presentation.field.presentation.placeholder=Présentez-vous : qui êtes-vous, votre parcours, vos intérêts...
presentation.field.presentation.help=Cette présentation sera visible par tous les lecteurs
presentation.field.contentDescription=Description de votre contenu
presentation.field.contentDescription.placeholder=Décrivez le type de contenu que vous publiez : science-fiction, recherche scientifique, thèmes abordés...
presentation.field.contentDescription.help=Aidez les lecteurs à comprendre le type d'articles que vous publiez
presentation.field.mainnetAddress=Adresse Bitcoin mainnet (pour le sponsoring)
presentation.field.mainnetAddress.placeholder=1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa
presentation.field.mainnetAddress.help=Adresse Bitcoin mainnet où vous recevrez les paiements de sponsoring (0.046 BTC hors frais par sponsoring)
presentation.validation.invalidAddress=Adresse Bitcoin invalide (doit commencer par 1, 3 ou bc1)
presentation.fallback.user=Utilisateur
# Filters
filters.clear=Effacer tout
filters.author=Par auteur
filters.author=Tous les auteurs
filters.sort=Trier par
filters.sort.newest=Plus récent
filters.sort.oldest=Plus ancien
@ -81,3 +103,69 @@ footer.privacy=Politique de confidentialité
common.loading=Chargement...
common.error=Erreur
common.back=Retour
common.open=Ouvrir
# Settings
settings.title=Paramètres
settings.keyManagement.title=Gestion des clés
settings.keyManagement.loading=Chargement...
settings.keyManagement.publicKey.npub=Clé publique (npub)
settings.keyManagement.publicKey.hex=Clé publique (hex)
settings.keyManagement.copy=Copier
settings.keyManagement.copied=✓ Copié
settings.keyManagement.noAccount.title=Aucun compte trouvé
settings.keyManagement.noAccount.description=Créez un nouveau compte en important une clé privée. La clé sera chiffrée à l'aide d'un système de chiffrement à deux niveaux.
settings.keyManagement.import.button.new=Importer une clé privée
settings.keyManagement.import.button.replace=Remplacer le compte (Importer une nouvelle clé)
settings.keyManagement.import.warning.title=⚠️ Important
settings.keyManagement.import.warning.description=Après l'import, vous recevrez <strong>4 mots de récupération</strong> (dictionnaire BIP39) pour sécuriser votre compte. Ces mots chiffrent une clé de chiffrement (KEK) stockée dans l'API Credentials du navigateur, qui chiffre ensuite votre clé privée stockée dans IndexedDB (système de chiffrement à deux niveaux).
settings.keyManagement.import.warning.replace=Avertissement : L'importation d'une nouvelle clé remplacera votre compte existant. Assurez-vous d'avoir sauvegardé votre phrase de récupération avant de continuer.
settings.keyManagement.import.label=Clé privée (URL nsec, nsec1..., ou hex)
settings.keyManagement.import.placeholder=nsec1... ou nostr://nsec1... ou clé hex
settings.keyManagement.import.help=Vous pouvez coller une clé nsec, une URL nostr:// contenant un nsec, ou une clé privée hex (64 caractères).
settings.keyManagement.import.error.required=Veuillez entrer une clé privée
settings.keyManagement.import.error.invalid=Format de clé invalide. Veuillez fournir un nsec (nsec1...) ou une clé privée hex (64 caractères).
settings.keyManagement.import.error.failed=Échec de l'importation de la clé
settings.keyManagement.replace.warning.title=⚠️ Remplacer le compte existant ?
settings.keyManagement.replace.warning.description=Cela supprimera votre compte actuel et créera un nouveau compte avec la clé importée. Assurez-vous d'avoir sauvegardé votre phrase de récupération pour le compte actuel.
settings.keyManagement.replace.cancel=Annuler
settings.keyManagement.replace.confirm=Remplacer le compte
settings.keyManagement.replace.replacing=Remplacement...
settings.keyManagement.import.cancel=Annuler
settings.keyManagement.import.importing=Importation...
settings.keyManagement.import.import=Importer
settings.keyManagement.recovery.warning.title=⚠️ Important
settings.keyManagement.recovery.warning.part1=Ces <strong>4 mots de récupération</strong> sont votre seul moyen de récupérer votre compte. <strong>Ils ne seront jamais affichés à nouveau.</strong>
settings.keyManagement.recovery.warning.part2=Ces mots (dictionnaire BIP39) sont utilisés avec <strong>PBKDF2</strong> pour chiffrer une clé de chiffrement (KEK) stockée dans l'API Credentials du navigateur. Cette KEK chiffre ensuite votre clé privée stockée dans IndexedDB (système à deux niveaux).
settings.keyManagement.recovery.warning.part3=Enregistrez-les dans un endroit sûr. Sans ces mots, vous perdrez définitivement l'accès à votre compte.
settings.keyManagement.recovery.copy=Copier les mots de récupération
settings.keyManagement.recovery.copied=✓ Copié !
settings.keyManagement.recovery.newNpub=Votre nouvelle clé publique (npub)
settings.keyManagement.recovery.done=Terminé
settings.nip95.title=NIP-95 Upload Endpoints
settings.nip95.loading=Chargement...
settings.nip95.error.loadFailed=Échec du chargement des API NIP-95
settings.nip95.error.updateFailed=Échec de la mise à jour de l'API
settings.nip95.error.priorityFailed=Échec de la mise à jour de la priorité
settings.nip95.error.urlFailed=Échec de la mise à jour de l'URL
settings.nip95.error.addFailed=Échec de l'ajout de l'API
settings.nip95.error.removeFailed=Échec de la suppression de l'API
settings.nip95.error.invalidUrl=Format d'URL invalide
settings.nip95.error.urlRequired=L'URL est requise
settings.nip95.addButton=Ajouter un endpoint
settings.nip95.add.url=URL de l'endpoint
settings.nip95.add.placeholder=https://example.com/api/upload
settings.nip95.add.add=Ajouter
settings.nip95.add.cancel=Annuler
settings.nip95.add.adding=Ajout...
settings.nip95.list.enabled=Activé
settings.nip95.list.disabled=Désactivé
settings.nip95.list.priority=Priorité
settings.nip95.list.url=URL
settings.nip95.list.actions=Actions
settings.nip95.list.edit=Modifier
settings.nip95.list.save=Enregistrer
settings.nip95.list.cancel=Annuler
settings.nip95.list.remove=Supprimer
settings.nip95.remove.confirm=Êtes-vous sûr de vouloir supprimer cet endpoint ?
settings.nip95.empty=Aucun endpoint configuré

View File

@ -3,12 +3,13 @@ import { PageHeader } from '@/components/PageHeader'
import { Footer } from '@/components/Footer'
import { Nip95ConfigManager } from '@/components/Nip95ConfigManager'
import { KeyManagementManager } from '@/components/KeyManagementManager'
import { t } from '@/lib/i18n'
export default function SettingsPage() {
return (
<>
<Head>
<title>Settings - zapwall.fr</title>
<title>{t('settings.title')} - zapwall.fr</title>
<meta name="description" content="Application settings and configuration" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" href="/favicon.svg" type="image/svg+xml" />
@ -16,7 +17,7 @@ export default function SettingsPage() {
<main className="min-h-screen bg-cyber-darker">
<PageHeader />
<div className="max-w-4xl mx-auto px-4 py-8">
<h1 className="text-3xl font-bold text-neon-cyan mb-8">Settings</h1>
<h1 className="text-3xl font-bold text-neon-cyan mb-8">{t('settings.title')}</h1>
<div className="space-y-8">
<KeyManagementManager />
<Nip95ConfigManager />

View File

@ -104,3 +104,68 @@ common.loading=Loading...
common.error=Error
common.back=Back
common.open=Open
# Settings
settings.title=Settings
settings.keyManagement.title=Key Management
settings.keyManagement.loading=Loading...
settings.keyManagement.publicKey.npub=Public Key (npub)
settings.keyManagement.publicKey.hex=Public Key (hex)
settings.keyManagement.copy=Copy
settings.keyManagement.copied=✓ Copied
settings.keyManagement.noAccount.title=No account found
settings.keyManagement.noAccount.description=Create a new account by importing a private key. The key will be encrypted using a two-level encryption system.
settings.keyManagement.import.button.new=Import Private Key
settings.keyManagement.import.button.replace=Replace Account (Import New Key)
settings.keyManagement.import.warning.title=⚠️ Important
settings.keyManagement.import.warning.description=After importing, you will receive <strong>4 recovery words</strong> (BIP39 dictionary) to secure your account. These words encrypt a Key Encryption Key (KEK) stored in the browser's Credentials API, which then encrypts your private key stored in IndexedDB (two-level encryption system).
settings.keyManagement.import.warning.replace=Warning: Importing a new key will replace your existing account. Make sure you have your recovery phrase saved before proceeding.
settings.keyManagement.import.label=Private Key (nsec URL, nsec1..., or hex)
settings.keyManagement.import.placeholder=nsec1... or nostr://nsec1... or hex key
settings.keyManagement.import.help=You can paste a nsec key, a nostr:// URL containing a nsec, or a hex private key (64 characters).
settings.keyManagement.import.error.required=Please enter a private key
settings.keyManagement.import.error.invalid=Invalid key format. Please provide a nsec (nsec1...) or hex (64 characters) private key.
settings.keyManagement.import.error.failed=Failed to import key
settings.keyManagement.replace.warning.title=⚠️ Replace Existing Account?
settings.keyManagement.replace.warning.description=This will delete your current account and create a new one with the imported key. Make sure you have saved your recovery phrase for the current account.
settings.keyManagement.replace.cancel=Cancel
settings.keyManagement.replace.confirm=Replace Account
settings.keyManagement.replace.replacing=Replacing...
settings.keyManagement.import.cancel=Cancel
settings.keyManagement.import.importing=Importing...
settings.keyManagement.import.import=Import
settings.keyManagement.recovery.warning.title=⚠️ Important
settings.keyManagement.recovery.warning.part1=These <strong>4 recovery words</strong> are your only way to recover your account. <strong>They will never be displayed again.</strong>
settings.keyManagement.recovery.warning.part2=These words (BIP39 dictionary) are used with <strong>PBKDF2</strong> to encrypt a Key Encryption Key (KEK) stored in the browser's Credentials API. This KEK then encrypts your private key stored in IndexedDB (two-level system).
settings.keyManagement.recovery.warning.part3=Save them in a safe place. Without these words, you will permanently lose access to your account.
settings.keyManagement.recovery.copy=Copy Recovery Words
settings.keyManagement.recovery.copied=✓ Copied!
settings.keyManagement.recovery.newNpub=Your new public key (npub)
settings.keyManagement.recovery.done=Done
settings.nip95.title=NIP-95 Upload Endpoints
settings.nip95.loading=Loading...
settings.nip95.error.loadFailed=Failed to load NIP-95 APIs
settings.nip95.error.updateFailed=Failed to update API
settings.nip95.error.priorityFailed=Failed to update priority
settings.nip95.error.urlFailed=Failed to update URL
settings.nip95.error.addFailed=Failed to add API
settings.nip95.error.removeFailed=Failed to remove API
settings.nip95.error.invalidUrl=Invalid URL format
settings.nip95.error.urlRequired=URL is required
settings.nip95.addButton=Add endpoint
settings.nip95.add.url=Endpoint URL
settings.nip95.add.placeholder=https://example.com/api/upload
settings.nip95.add.add=Add
settings.nip95.add.cancel=Cancel
settings.nip95.add.adding=Adding...
settings.nip95.list.enabled=Enabled
settings.nip95.list.disabled=Disabled
settings.nip95.list.priority=Priority
settings.nip95.list.url=URL
settings.nip95.list.actions=Actions
settings.nip95.list.edit=Edit
settings.nip95.list.save=Save
settings.nip95.list.cancel=Cancel
settings.nip95.list.remove=Remove
settings.nip95.remove.confirm=Are you sure you want to remove this endpoint?
settings.nip95.empty=No endpoints configured

View File

@ -103,3 +103,69 @@ footer.privacy=Politique de confidentialité
common.loading=Chargement...
common.error=Erreur
common.back=Retour
common.open=Ouvrir
# Settings
settings.title=Paramètres
settings.keyManagement.title=Gestion des clés
settings.keyManagement.loading=Chargement...
settings.keyManagement.publicKey.npub=Clé publique (npub)
settings.keyManagement.publicKey.hex=Clé publique (hex)
settings.keyManagement.copy=Copier
settings.keyManagement.copied=✓ Copié
settings.keyManagement.noAccount.title=Aucun compte trouvé
settings.keyManagement.noAccount.description=Créez un nouveau compte en important une clé privée. La clé sera chiffrée à l'aide d'un système de chiffrement à deux niveaux.
settings.keyManagement.import.button.new=Importer une clé privée
settings.keyManagement.import.button.replace=Remplacer le compte (Importer une nouvelle clé)
settings.keyManagement.import.warning.title=⚠️ Important
settings.keyManagement.import.warning.description=Après l'import, vous recevrez <strong>4 mots de récupération</strong> (dictionnaire BIP39) pour sécuriser votre compte. Ces mots chiffrent une clé de chiffrement (KEK) stockée dans l'API Credentials du navigateur, qui chiffre ensuite votre clé privée stockée dans IndexedDB (système de chiffrement à deux niveaux).
settings.keyManagement.import.warning.replace=Avertissement : L'importation d'une nouvelle clé remplacera votre compte existant. Assurez-vous d'avoir sauvegardé votre phrase de récupération avant de continuer.
settings.keyManagement.import.label=Clé privée (URL nsec, nsec1..., ou hex)
settings.keyManagement.import.placeholder=nsec1... ou nostr://nsec1... ou clé hex
settings.keyManagement.import.help=Vous pouvez coller une clé nsec, une URL nostr:// contenant un nsec, ou une clé privée hex (64 caractères).
settings.keyManagement.import.error.required=Veuillez entrer une clé privée
settings.keyManagement.import.error.invalid=Format de clé invalide. Veuillez fournir un nsec (nsec1...) ou une clé privée hex (64 caractères).
settings.keyManagement.import.error.failed=Échec de l'importation de la clé
settings.keyManagement.replace.warning.title=⚠️ Remplacer le compte existant ?
settings.keyManagement.replace.warning.description=Cela supprimera votre compte actuel et créera un nouveau compte avec la clé importée. Assurez-vous d'avoir sauvegardé votre phrase de récupération pour le compte actuel.
settings.keyManagement.replace.cancel=Annuler
settings.keyManagement.replace.confirm=Remplacer le compte
settings.keyManagement.replace.replacing=Remplacement...
settings.keyManagement.import.cancel=Annuler
settings.keyManagement.import.importing=Importation...
settings.keyManagement.import.import=Importer
settings.keyManagement.recovery.warning.title=⚠️ Important
settings.keyManagement.recovery.warning.part1=Ces <strong>4 mots de récupération</strong> sont votre seul moyen de récupérer votre compte. <strong>Ils ne seront jamais affichés à nouveau.</strong>
settings.keyManagement.recovery.warning.part2=Ces mots (dictionnaire BIP39) sont utilisés avec <strong>PBKDF2</strong> pour chiffrer une clé de chiffrement (KEK) stockée dans l'API Credentials du navigateur. Cette KEK chiffre ensuite votre clé privée stockée dans IndexedDB (système à deux niveaux).
settings.keyManagement.recovery.warning.part3=Enregistrez-les dans un endroit sûr. Sans ces mots, vous perdrez définitivement l'accès à votre compte.
settings.keyManagement.recovery.copy=Copier les mots de récupération
settings.keyManagement.recovery.copied=✓ Copié !
settings.keyManagement.recovery.newNpub=Votre nouvelle clé publique (npub)
settings.keyManagement.recovery.done=Terminé
settings.nip95.title=NIP-95 Upload Endpoints
settings.nip95.loading=Chargement...
settings.nip95.error.loadFailed=Échec du chargement des API NIP-95
settings.nip95.error.updateFailed=Échec de la mise à jour de l'API
settings.nip95.error.priorityFailed=Échec de la mise à jour de la priorité
settings.nip95.error.urlFailed=Échec de la mise à jour de l'URL
settings.nip95.error.addFailed=Échec de l'ajout de l'API
settings.nip95.error.removeFailed=Échec de la suppression de l'API
settings.nip95.error.invalidUrl=Format d'URL invalide
settings.nip95.error.urlRequired=L'URL est requise
settings.nip95.addButton=Ajouter un endpoint
settings.nip95.add.url=URL de l'endpoint
settings.nip95.add.placeholder=https://example.com/api/upload
settings.nip95.add.add=Ajouter
settings.nip95.add.cancel=Annuler
settings.nip95.add.adding=Ajout...
settings.nip95.list.enabled=Activé
settings.nip95.list.disabled=Désactivé
settings.nip95.list.priority=Priorité
settings.nip95.list.url=URL
settings.nip95.list.actions=Actions
settings.nip95.list.edit=Modifier
settings.nip95.list.save=Enregistrer
settings.nip95.list.cancel=Annuler
settings.nip95.list.remove=Supprimer
settings.nip95.remove.confirm=Êtes-vous sûr de vouloir supprimer cet endpoint ?
settings.nip95.empty=Aucun endpoint configuré