Complete English translation for presentation page and update Bitcoin address help text

- Translate all hardcoded French text in presentation form to use i18n
- Update Bitcoin address help text to specify '0.046 BTC excluding fees'
- Add all presentation field translations (labels, placeholders, help texts)
- Add image upload field translations
- Add validation error message translation
- Add fallback user name translation
- All TypeScript checks pass
This commit is contained in:
Nicolas Cantu 2025-12-28 16:15:28 +01:00
parent 58e1ace081
commit f082befb36
3 changed files with 43 additions and 23 deletions

View File

@ -61,14 +61,14 @@ function PresentationField({
return ( return (
<ArticleField <ArticleField
id="presentation" id="presentation"
label="Présentation personnelle" label={t('presentation.field.presentation')}
value={draft.presentation} value={draft.presentation}
onChange={(value) => onChange({ ...draft, presentation: value as string })} onChange={(value) => onChange({ ...draft, presentation: value as string })}
required required
type="textarea" type="textarea"
rows={6} rows={6}
placeholder="Présentez-vous : qui êtes-vous, votre parcours, vos intérêts..." placeholder={t('presentation.field.presentation.placeholder')}
helpText="Cette présentation sera visible par tous les lecteurs" helpText={t('presentation.field.presentation.help')}
/> />
) )
} }
@ -83,14 +83,14 @@ function ContentDescriptionField({
return ( return (
<ArticleField <ArticleField
id="contentDescription" id="contentDescription"
label="Description de votre contenu" label={t('presentation.field.contentDescription')}
value={draft.contentDescription} value={draft.contentDescription}
onChange={(value) => onChange({ ...draft, contentDescription: value as string })} onChange={(value) => onChange({ ...draft, contentDescription: value as string })}
required required
type="textarea" type="textarea"
rows={6} rows={6}
placeholder="Décrivez le type de contenu que vous publiez : science-fiction, recherche scientifique, thèmes abordés..." placeholder={t('presentation.field.contentDescription.placeholder')}
helpText="Aidez les lecteurs à comprendre le type d'articles que vous publiez" helpText={t('presentation.field.contentDescription.help')}
/> />
) )
} }
@ -105,13 +105,13 @@ function MainnetAddressField({
return ( return (
<ArticleField <ArticleField
id="mainnetAddress" id="mainnetAddress"
label="Adresse Bitcoin mainnet (pour le sponsoring)" label={t('presentation.field.mainnetAddress')}
value={draft.mainnetAddress} value={draft.mainnetAddress}
onChange={(value) => onChange({ ...draft, mainnetAddress: value as string })} onChange={(value) => onChange({ ...draft, mainnetAddress: value as string })}
required required
type="text" type="text"
placeholder="1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa" placeholder={t('presentation.field.mainnetAddress.placeholder')}
helpText="Adresse Bitcoin mainnet où vous recevrez les paiements de sponsoring (0.046 BTC par sponsoring)" helpText={t('presentation.field.mainnetAddress.help')}
/> />
) )
} }
@ -126,10 +126,8 @@ function PictureField({
return ( return (
<ImageUploadField <ImageUploadField
id="picture" id="picture"
label="Photo de profil"
value={draft.pictureUrl} value={draft.pictureUrl}
onChange={(url) => onChange({ ...draft, pictureUrl: url })} onChange={(url) => onChange({ ...draft, pictureUrl: url })}
helpText="Image de profil pour votre page auteur (max 5Mo, formats: PNG, JPG, WebP)"
/> />
) )
} }
@ -206,7 +204,7 @@ function useAuthorPresentationState(pubkey: string | null) {
e.preventDefault() e.preventDefault()
const address = draft.mainnetAddress.trim() const address = draft.mainnetAddress.trim()
if (!ADDRESS_PATTERN.test(address)) { if (!ADDRESS_PATTERN.test(address)) {
setValidationError('Adresse Bitcoin invalide (doit commencer par 1, 3 ou bc1)') setValidationError(t('presentation.validation.invalidAddress'))
return return
} }
setValidationError(null) setValidationError(null)
@ -237,7 +235,7 @@ function AuthorPresentationFormView({
} }
// Get user name or fallback to shortened pubkey // Get user name or fallback to shortened pubkey
const userName = profile?.name ?? (pubkey ? `${pubkey.substring(0, 16)}...` : 'Utilisateur') const userName = profile?.name ?? (pubkey ? `${pubkey.substring(0, 16)}...` : t('presentation.fallback.user'))
return ( return (
<PresentationForm <PresentationForm

View File

@ -1,10 +1,11 @@
import { useState } from 'react' import { useState } from 'react'
import { uploadNip95Media } from '@/lib/nip95' import { uploadNip95Media } from '@/lib/nip95'
import { t } from '@/lib/i18n'
import Image from 'next/image' import Image from 'next/image'
interface ImageUploadFieldProps { interface ImageUploadFieldProps {
id: string id: string
label: string label?: string | undefined
value?: string | undefined value?: string | undefined
onChange: (url: string) => void onChange: (url: string) => void
helpText?: string | undefined helpText?: string | undefined
@ -28,25 +29,28 @@ export function ImageUploadField({ id, label, value, onChange, helpText }: Image
if (media.type === 'image') { if (media.type === 'image') {
onChange(media.url) onChange(media.url)
} else { } else {
setError('Seules les images sont autorisées') setError(t('presentation.field.picture.error.imagesOnly'))
} }
} catch (e) { } catch (e) {
setError(e instanceof Error ? e.message : 'Erreur lors de l\'upload') setError(e instanceof Error ? e.message : t('presentation.field.picture.error.uploadFailed'))
} finally { } finally {
setUploading(false) setUploading(false)
} }
} }
const displayLabel = label ?? t('presentation.field.picture')
const displayHelpText = helpText ?? t('presentation.field.picture.help')
return ( return (
<div className="space-y-2"> <div className="space-y-2">
<label htmlFor={id} className="block text-sm font-medium text-neon-cyan"> <label htmlFor={id} className="block text-sm font-medium text-neon-cyan">
{label} {displayLabel}
</label> </label>
{value && ( {value && (
<div className="relative w-32 h-32 rounded-lg overflow-hidden border border-neon-cyan/20"> <div className="relative w-32 h-32 rounded-lg overflow-hidden border border-neon-cyan/20">
<Image <Image
src={value} src={value}
alt="Profile picture" alt={t('presentation.field.picture')}
fill fill
className="object-cover" className="object-cover"
/> />
@ -57,7 +61,7 @@ export function ImageUploadField({ id, label, value, onChange, helpText }: Image
htmlFor={id} htmlFor={id}
className="px-4 py-2 bg-neon-cyan/20 hover:bg-neon-cyan/30 text-neon-cyan rounded-lg text-sm font-medium transition-all border border-neon-cyan/50 hover:shadow-glow-cyan cursor-pointer" className="px-4 py-2 bg-neon-cyan/20 hover:bg-neon-cyan/30 text-neon-cyan rounded-lg text-sm font-medium transition-all border border-neon-cyan/50 hover:shadow-glow-cyan cursor-pointer"
> >
{uploading ? 'Upload en cours...' : value ? 'Changer l\'image' : 'Télécharger une image'} {uploading ? t('presentation.field.picture.uploading') : value ? t('presentation.field.picture.change') : t('presentation.field.picture.upload')}
</label> </label>
<input <input
id={id} id={id}
@ -73,17 +77,16 @@ export function ImageUploadField({ id, label, value, onChange, helpText }: Image
onClick={() => onChange('')} onClick={() => onChange('')}
className="px-4 py-2 bg-red-500/20 hover:bg-red-500/30 text-red-400 rounded-lg text-sm font-medium transition-all border border-red-500/50" className="px-4 py-2 bg-red-500/20 hover:bg-red-500/30 text-red-400 rounded-lg text-sm font-medium transition-all border border-red-500/50"
> >
Supprimer {t('presentation.field.picture.remove')}
</button> </button>
)} )}
</div> </div>
{error && ( {error && (
<p className="text-sm text-red-400">{error}</p> <p className="text-sm text-red-400">{error}</p>
)} )}
{helpText && ( {displayHelpText && (
<p className="text-sm text-cyber-accent">{helpText}</p> <p className="text-sm text-cyber-accent">{displayHelpText}</p>
)} )}
</div> </div>
) )
} }

View File

@ -61,6 +61,25 @@ presentation.success=Presentation article created!
presentation.successMessage=Your presentation article has been created successfully. You can now publish articles. presentation.successMessage=Your presentation article has been created successfully. You can now publish articles.
presentation.notConnected=Connect with Nostr to create your presentation article presentation.notConnected=Connect with Nostr to create your presentation article
presentation.profileNote=This profile data is specific to zapwall.fr and may differ from your Nostr profile. presentation.profileNote=This profile data is specific to zapwall.fr and may differ from your Nostr profile.
presentation.field.picture=Profile picture
presentation.field.picture.help=Profile image for your author page (max 5MB, formats: PNG, JPG, WebP)
presentation.field.picture.change=Change image
presentation.field.picture.upload=Upload an image
presentation.field.picture.uploading=Uploading...
presentation.field.picture.remove=Remove
presentation.field.picture.error.imagesOnly=Only images are allowed
presentation.field.picture.error.uploadFailed=Upload error
presentation.field.presentation=Personal presentation
presentation.field.presentation.placeholder=Introduce yourself: who you are, your background, your interests...
presentation.field.presentation.help=This presentation will be visible to all readers
presentation.field.contentDescription=Content description
presentation.field.contentDescription.placeholder=Describe the type of content you publish: science fiction, scientific research, themes covered...
presentation.field.contentDescription.help=Help readers understand the type of articles you publish
presentation.field.mainnetAddress=Bitcoin mainnet address (for sponsoring)
presentation.field.mainnetAddress.placeholder=1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa
presentation.field.mainnetAddress.help=Bitcoin mainnet address where you will receive sponsoring payments (0.046 BTC excluding fees per sponsoring)
presentation.validation.invalidAddress=Invalid Bitcoin address (must start with 1, 3 or bc1)
presentation.fallback.user=User
# Filters # Filters
filters.clear=Clear all filters.clear=Clear all