i18n(es): translate shared component literals (camera, quality meter, entity picker, offline, comments)
This commit is contained in:
parent
c7a6421be1
commit
7e578fb7d9
7 changed files with 192 additions and 65 deletions
|
|
@ -22,6 +22,7 @@ import {
|
|||
} from '@/components/ui/dialog';
|
||||
import { useToast } from '@/hooks/useToast';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { useTranslation } from '@/contexts/language';
|
||||
import {
|
||||
Camera,
|
||||
RotateCcw,
|
||||
|
|
@ -106,6 +107,7 @@ export function CameraCapture({
|
|||
onCapture,
|
||||
isUploading,
|
||||
}: CameraCaptureProps) {
|
||||
const { t } = useTranslation();
|
||||
const { toast } = useToast();
|
||||
const videoRef = useRef<HTMLVideoElement>(null);
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||
|
|
@ -134,14 +136,14 @@ export function CameraCapture({
|
|||
return;
|
||||
}
|
||||
toast({
|
||||
title: 'Camera error',
|
||||
description: 'Could not open the camera. Please check app permissions in Settings.',
|
||||
title: t('miscUi.cameraErrorTitle'),
|
||||
description: t('miscUi.cameraErrorDesc'),
|
||||
variant: 'destructive',
|
||||
});
|
||||
} finally {
|
||||
setIsCapturingNative(false);
|
||||
}
|
||||
}, [onCapture, onOpenChange, toast]);
|
||||
}, [onCapture, onOpenChange, toast, t]);
|
||||
|
||||
// ── Browser camera ────────────────────────────────────────────────────────
|
||||
const startCamera = useCallback(async (facing: 'user' | 'environment' = facingMode) => {
|
||||
|
|
@ -164,14 +166,14 @@ export function CameraCapture({
|
|||
} catch {
|
||||
setHasPermission(false);
|
||||
toast({
|
||||
title: 'Camera access denied',
|
||||
description: 'Please allow camera access in your browser settings.',
|
||||
title: t('miscUi.cameraDeniedTitle'),
|
||||
description: t('miscUi.cameraDeniedDesc'),
|
||||
variant: 'destructive',
|
||||
});
|
||||
} finally {
|
||||
setIsStarting(false);
|
||||
}
|
||||
}, [facingMode, toast]);
|
||||
}, [facingMode, toast, t]);
|
||||
|
||||
const stopCamera = useCallback(() => {
|
||||
streamRef.current?.getTracks().forEach(track => track.stop());
|
||||
|
|
@ -239,13 +241,13 @@ export function CameraCapture({
|
|||
return (
|
||||
<Dialog open={open} onOpenChange={handleClose}>
|
||||
<DialogContent className="sm:max-w-[340px] bg-black border-[#ff0040]/50">
|
||||
<DialogTitle className="sr-only">Opening Camera</DialogTitle>
|
||||
<DialogTitle className="sr-only">{t('miscUi.openingCamera')}</DialogTitle>
|
||||
<div className="flex flex-col items-center justify-center py-12 gap-4">
|
||||
<div className="w-16 h-16 rounded-full bg-[#ff0040]/20 flex items-center justify-center">
|
||||
<Camera className="h-8 w-8 text-[#ff0040]" />
|
||||
</div>
|
||||
<p className="text-white font-bold text-center">Opening camera…</p>
|
||||
<p className="text-gray-400 text-sm text-center">Use your phone camera, then return here</p>
|
||||
<p className="text-white font-bold text-center">{t('miscUi.openingCamera')}</p>
|
||||
<p className="text-gray-400 text-sm text-center">{t('miscUi.usePhoneCamera')}</p>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
|
@ -271,7 +273,7 @@ export function CameraCapture({
|
|||
<div className="absolute inset-0 flex items-center justify-center bg-black">
|
||||
<div className="text-center">
|
||||
<Loader2 className="h-8 w-8 animate-spin text-[#ccff00] mx-auto mb-2" />
|
||||
<p className="text-gray-400 text-sm">Starting camera...</p>
|
||||
<p className="text-gray-400 text-sm">{t('miscUi.startingCamera')}</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -282,21 +284,21 @@ export function CameraCapture({
|
|||
<div className="text-center p-6 space-y-4">
|
||||
<Camera className="h-12 w-12 text-gray-600 mx-auto" />
|
||||
<div>
|
||||
<h3 className="text-white font-bold mb-2">Camera Access Required</h3>
|
||||
<h3 className="text-white font-bold mb-2">{t('miscUi.cameraAccessRequired')}</h3>
|
||||
<p className="text-gray-400 text-sm">
|
||||
Allow camera access in your browser settings, or use the file picker below.
|
||||
{t('miscUi.cameraAccessHint')}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Button onClick={() => startCamera()} className="bg-[#ff0040] hover:bg-[#ff0040]/90 gap-2">
|
||||
<Camera className="h-4 w-4" />
|
||||
Try Again
|
||||
{t('common.tryAgain')}
|
||||
</Button>
|
||||
<label className="cursor-pointer">
|
||||
<Button variant="outline" className="gap-2 w-full border-gray-700" asChild>
|
||||
<span>
|
||||
<Upload className="h-4 w-4" />
|
||||
Upload from Gallery
|
||||
{t('miscUi.uploadFromGallery')}
|
||||
</span>
|
||||
</Button>
|
||||
<input
|
||||
|
|
@ -400,9 +402,9 @@ export function CameraCapture({
|
|||
className="gap-2 bg-[#ccff00] hover:bg-[#ccff00]/90 text-black font-bold"
|
||||
>
|
||||
{isUploading ? (
|
||||
<><Loader2 className="h-4 w-4 animate-spin" />Uploading...</>
|
||||
<><Loader2 className="h-4 w-4 animate-spin" />{t('miscUi.uploading')}</>
|
||||
) : (
|
||||
<><Check className="h-4 w-4" />Use Photo</>
|
||||
<><Check className="h-4 w-4" />{t('miscUi.usePhoto')}</>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import { useAuthor } from '@/hooks/useAuthor';
|
|||
import { genUserName } from '@/lib/genUserName';
|
||||
import type { EntityType } from '@/lib/types';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { useTranslation } from '@/contexts/language';
|
||||
|
||||
interface EntityTypeFieldProps {
|
||||
entityType: EntityType;
|
||||
|
|
@ -22,6 +23,7 @@ interface EntityTypeFieldProps {
|
|||
}
|
||||
|
||||
function MemberRow({ pubkey, onRemove }: { pubkey: string; onRemove: () => void }) {
|
||||
const { t } = useTranslation();
|
||||
const author = useAuthor(pubkey);
|
||||
const metadata = author.data?.metadata;
|
||||
const name = metadata?.name ?? genUserName(pubkey);
|
||||
|
|
@ -43,7 +45,7 @@ function MemberRow({ pubkey, onRemove }: { pubkey: string; onRemove: () => void
|
|||
size="icon"
|
||||
className="h-7 w-7 shrink-0 text-gray-400 hover:text-red-400"
|
||||
onClick={onRemove}
|
||||
aria-label="Remove member"
|
||||
aria-label={t('miscUi.removeMember')}
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</Button>
|
||||
|
|
@ -65,6 +67,7 @@ export function EntityTypeField({
|
|||
accentBg = 'bg-indigo-600',
|
||||
accentText = 'text-indigo-400',
|
||||
}: EntityTypeFieldProps) {
|
||||
const { t } = useTranslation();
|
||||
const [input, setInput] = useState('');
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
|
|
@ -87,11 +90,11 @@ export function EntityTypeField({
|
|||
}
|
||||
|
||||
if (!hex) {
|
||||
setError('Enter a valid npub, nprofile, or 64-char hex pubkey.');
|
||||
setError(t('miscUi.entityInvalidPubkey'));
|
||||
return;
|
||||
}
|
||||
if (members.includes(hex)) {
|
||||
setError('That member is already added.');
|
||||
setError(t('miscUi.entityAlreadyAdded'));
|
||||
return;
|
||||
}
|
||||
onMembersChange([...members, hex]);
|
||||
|
|
@ -102,7 +105,7 @@ export function EntityTypeField({
|
|||
return (
|
||||
<div className="space-y-4 rounded-lg border border-gray-700 bg-gray-900/60 p-4">
|
||||
<div className="space-y-2">
|
||||
<Label className="text-base font-bold">Who is this profile for?</Label>
|
||||
<Label className="text-base font-bold">{t('miscUi.whoIsThisFor')}</Label>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<button
|
||||
type="button"
|
||||
|
|
@ -115,8 +118,8 @@ export function EntityTypeField({
|
|||
)}
|
||||
>
|
||||
<User className="h-6 w-6" />
|
||||
<span className="text-sm font-bold">Individual</span>
|
||||
<span className="text-[11px] opacity-80">A single person</span>
|
||||
<span className="text-sm font-bold">{t('miscUi.individual')}</span>
|
||||
<span className="text-[11px] opacity-80">{t('miscUi.individualDesc')}</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
|
|
@ -129,8 +132,8 @@ export function EntityTypeField({
|
|||
)}
|
||||
>
|
||||
<Users className="h-6 w-6" />
|
||||
<span className="text-sm font-bold">Collective</span>
|
||||
<span className="text-[11px] opacity-80">Crew, studio or company</span>
|
||||
<span className="text-sm font-bold">{t('miscUi.collective')}</span>
|
||||
<span className="text-[11px] opacity-80">{t('miscUi.collectiveDesc')}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -139,11 +142,10 @@ export function EntityTypeField({
|
|||
<div className="space-y-3">
|
||||
<div>
|
||||
<Label className={cn('flex items-center gap-2', accentText)}>
|
||||
<Users className="h-4 w-4" /> Members
|
||||
<Users className="h-4 w-4" /> {t('miscUi.membersLabel')}
|
||||
</Label>
|
||||
<p className="mt-1 text-xs text-gray-500">
|
||||
Add the Nostr accounts of your crew. Each member confirms their own
|
||||
spot with their key — until then they show as “pending”.
|
||||
{t('miscUi.entityMembersHint')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
|
@ -166,7 +168,7 @@ export function EntityTypeField({
|
|||
className="bg-gray-800 border-gray-700 font-mono text-sm"
|
||||
/>
|
||||
<Button type="button" onClick={addMember} className={cn('shrink-0 gap-1', accentBg)}>
|
||||
<Plus className="h-4 w-4" /> Add
|
||||
<Plus className="h-4 w-4" /> {t('common.add')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
|
|
@ -183,8 +185,7 @@ export function EntityTypeField({
|
|||
<p className="flex items-start gap-1.5 rounded-lg border border-gray-700 bg-black/40 p-2 text-xs text-gray-400">
|
||||
<ShieldCheck className={cn('mt-0.5 h-3.5 w-3.5 shrink-0', accentText)} />
|
||||
<span>
|
||||
Membership is verified per member — nobody can be added to your crew
|
||||
without confirming from their own account.
|
||||
{t('miscUi.entityVerifiedHint')}
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { Button } from '@/components/ui/button';
|
|||
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
|
||||
import { Wifi, WifiOff, Cloud, CloudOff, Loader2 } from 'lucide-react';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { useTranslation } from '@/contexts/language';
|
||||
|
||||
interface OfflineIndicatorProps {
|
||||
className?: string;
|
||||
|
|
@ -11,6 +12,7 @@ interface OfflineIndicatorProps {
|
|||
}
|
||||
|
||||
export function OfflineIndicator({ className, showWhenOnline = false }: OfflineIndicatorProps) {
|
||||
const { t } = useTranslation();
|
||||
const isOnline = useOnlineStatus();
|
||||
const { data: pendingCount } = usePendingPostsCount();
|
||||
const { isSyncing } = useSyncPendingPosts();
|
||||
|
|
@ -41,7 +43,7 @@ export function OfflineIndicator({ className, showWhenOnline = false }: OfflineI
|
|||
) : (
|
||||
<WifiOff className="h-4 w-4" />
|
||||
)}
|
||||
{!isOnline && <span className="text-xs">Offline</span>}
|
||||
{!isOnline && <span className="text-xs">{t('miscUi.offline')}</span>}
|
||||
{pendingCount !== undefined && pendingCount > 0 && (
|
||||
<Badge variant="secondary" className="h-5 px-1.5 text-xs bg-orange-500/20 text-orange-400">
|
||||
{pendingCount}
|
||||
|
|
@ -65,12 +67,12 @@ export function OfflineIndicator({ className, showWhenOnline = false }: OfflineI
|
|||
</div>
|
||||
<div>
|
||||
<p className="font-bold text-white">
|
||||
{isOnline ? 'Connected' : 'Offline Mode'}
|
||||
{isOnline ? t('miscUi.connected') : t('miscUi.offlineMode')}
|
||||
</p>
|
||||
<p className="text-xs text-gray-400">
|
||||
{isOnline
|
||||
? 'All features available'
|
||||
: 'Browsing cached spots'}
|
||||
? t('miscUi.allFeaturesAvailable')
|
||||
: t('miscUi.browsingCachedSpots')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -80,19 +82,19 @@ export function OfflineIndicator({ className, showWhenOnline = false }: OfflineI
|
|||
<div className="p-3 rounded-lg bg-orange-500/10 border border-orange-500/30">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm text-orange-400 font-medium">
|
||||
{pendingCount} queued action{pendingCount > 1 ? 's' : ''}
|
||||
{t('miscUi.queuedActions', { count: pendingCount })}
|
||||
</span>
|
||||
{isSyncing && (
|
||||
<Badge className="bg-orange-500 text-white text-xs">
|
||||
<Loader2 className="h-3 w-3 mr-1 animate-spin" />
|
||||
Syncing
|
||||
{t('miscUi.syncing')}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-xs text-gray-400 mt-1">
|
||||
{isOnline
|
||||
? 'Publishing when connection stabilizes...'
|
||||
: 'Will be published when back online'}
|
||||
? t('miscUi.publishingWhenStable')
|
||||
: t('miscUi.willPublishOnline')}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -100,11 +102,11 @@ export function OfflineIndicator({ className, showWhenOnline = false }: OfflineI
|
|||
{/* Offline Features Info */}
|
||||
{!isOnline && (
|
||||
<div className="text-xs text-gray-400 space-y-1">
|
||||
<p className="font-medium text-gray-300">Available offline:</p>
|
||||
<p className="font-medium text-gray-300">{t('miscUi.availableOffline')}</p>
|
||||
<ul className="list-disc list-inside space-y-0.5">
|
||||
<li>Browse previously viewed spots</li>
|
||||
<li>View cached spot details</li>
|
||||
<li>Keep acting — posts, RSVPs, listings and messages are queued and send when you reconnect</li>
|
||||
<li>{t('miscUi.offlineBrowseSpots')}</li>
|
||||
<li>{t('miscUi.offlineViewDetails')}</li>
|
||||
<li>{t('miscUi.offlineKeepActing')}</li>
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import {
|
|||
Circle,
|
||||
} from 'lucide-react';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { useTranslation } from '@/contexts/language';
|
||||
|
||||
export interface QualityCheckItem {
|
||||
id: string;
|
||||
|
|
@ -51,40 +52,41 @@ export function QualityMeter({
|
|||
hasSpotType,
|
||||
className,
|
||||
}: QualityMeterProps) {
|
||||
const { t } = useTranslation();
|
||||
const checks = useMemo(() => {
|
||||
const items: QualityCheckItem[] = [
|
||||
{
|
||||
id: 'location',
|
||||
label: 'Location added',
|
||||
label: t('miscUi.qualityLocationAdded'),
|
||||
points: 20,
|
||||
completed: hasLocation,
|
||||
icon: MapPin,
|
||||
},
|
||||
{
|
||||
id: 'photo1',
|
||||
label: 'At least 1 photo',
|
||||
label: t('miscUi.qualityOnePhoto'),
|
||||
points: 20,
|
||||
completed: hasPhotos,
|
||||
icon: Camera,
|
||||
},
|
||||
{
|
||||
id: 'photo3',
|
||||
label: '3+ photos',
|
||||
description: 'More angles = better',
|
||||
label: t('miscUi.qualityThreePhotos'),
|
||||
description: t('miscUi.qualityMoreAngles'),
|
||||
points: 15,
|
||||
completed: photoCount >= 3,
|
||||
icon: Camera,
|
||||
},
|
||||
{
|
||||
id: 'description',
|
||||
label: 'Description',
|
||||
label: t('miscUi.qualityDescription'),
|
||||
points: 15,
|
||||
completed: hasDescription && descriptionLength >= 20,
|
||||
icon: FileText,
|
||||
},
|
||||
{
|
||||
id: 'description-detailed',
|
||||
label: 'Detailed description (100+ chars)',
|
||||
label: t('miscUi.qualityDetailedDesc'),
|
||||
points: 10,
|
||||
completed: descriptionLength >= 100,
|
||||
icon: FileText,
|
||||
|
|
@ -95,15 +97,15 @@ export function QualityMeter({
|
|||
items.push(
|
||||
{
|
||||
id: 'contact',
|
||||
label: 'Contact info',
|
||||
description: 'Phone or website',
|
||||
label: t('miscUi.qualityContact'),
|
||||
description: t('miscUi.qualityPhoneOrWebsite'),
|
||||
points: 10,
|
||||
completed: !!hasPhone || !!hasWebsite,
|
||||
icon: Phone,
|
||||
},
|
||||
{
|
||||
id: 'hours',
|
||||
label: 'Opening hours',
|
||||
label: t('miscUi.qualityHours'),
|
||||
points: 10,
|
||||
completed: !!hasHours,
|
||||
icon: Clock,
|
||||
|
|
@ -112,7 +114,7 @@ export function QualityMeter({
|
|||
} else {
|
||||
items.push({
|
||||
id: 'spot-type',
|
||||
label: 'Spot type selected',
|
||||
label: t('miscUi.qualitySpotType'),
|
||||
points: 10,
|
||||
completed: !!hasSpotType,
|
||||
icon: MapPin,
|
||||
|
|
@ -120,7 +122,7 @@ export function QualityMeter({
|
|||
}
|
||||
|
||||
return items;
|
||||
}, [type, hasLocation, hasPhotos, photoCount, hasDescription, descriptionLength, hasPhone, hasWebsite, hasHours, hasSpotType]);
|
||||
}, [type, hasLocation, hasPhotos, photoCount, hasDescription, descriptionLength, hasPhone, hasWebsite, hasHours, hasSpotType, t]);
|
||||
|
||||
const { score, maxScore } = useMemo(() => {
|
||||
const completed = checks.filter(c => c.completed);
|
||||
|
|
@ -151,10 +153,10 @@ export function QualityMeter({
|
|||
};
|
||||
|
||||
const getScoreLabel = () => {
|
||||
if (percentage >= 80) return 'Excellent!';
|
||||
if (percentage >= 50) return 'Good';
|
||||
if (percentage >= 30) return 'Basic';
|
||||
return 'Getting started';
|
||||
if (percentage >= 80) return t('miscUi.qualityExcellent');
|
||||
if (percentage >= 50) return t('miscUi.qualityGood');
|
||||
if (percentage >= 30) return t('miscUi.qualityBasic');
|
||||
return t('miscUi.qualityGettingStarted');
|
||||
};
|
||||
|
||||
return (
|
||||
|
|
@ -163,7 +165,7 @@ export function QualityMeter({
|
|||
<div className="flex items-center justify-between mb-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<Zap className="h-4 w-4 text-[#ccff00]" />
|
||||
<span className="text-sm font-bold uppercase text-gray-300">Earning Potential</span>
|
||||
<span className="text-sm font-bold uppercase text-gray-300">{t('miscUi.earningPotential')}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className={cn('text-sm font-bold', getScoreColor())}>
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
|||
import { Skeleton } from '@/components/ui/skeleton';
|
||||
import { MessageSquare } from 'lucide-react';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { useTranslation } from '@/contexts/language';
|
||||
import type { NostrEvent } from '@nostrify/nostrify';
|
||||
import { CommentForm } from './CommentForm';
|
||||
import { Comment } from './Comment';
|
||||
|
|
@ -18,22 +19,27 @@ interface CommentsSectionProps {
|
|||
|
||||
export function CommentsSection({
|
||||
root,
|
||||
title = "Comments",
|
||||
emptyStateMessage = "No comments yet",
|
||||
emptyStateSubtitle = "Be the first to share your thoughts!",
|
||||
title,
|
||||
emptyStateMessage,
|
||||
emptyStateSubtitle,
|
||||
className,
|
||||
limit = 500,
|
||||
}: CommentsSectionProps) {
|
||||
const { t } = useTranslation();
|
||||
const { data: commentsData, isLoading, error } = useComments(root, limit);
|
||||
const comments = commentsData?.topLevelComments || [];
|
||||
|
||||
const resolvedTitle = title ?? t('miscUi.commentsTitle');
|
||||
const resolvedEmpty = emptyStateMessage ?? t('miscUi.noCommentsYet');
|
||||
const resolvedEmptySub = emptyStateSubtitle ?? t('miscUi.beFirstComment');
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<Card className="rounded-none sm:rounded-lg mx-0 sm:mx-0">
|
||||
<CardContent className="px-2 py-6 sm:p-6">
|
||||
<div className="text-center text-muted-foreground">
|
||||
<MessageSquare className="h-8 w-8 mx-auto mb-2 opacity-50" />
|
||||
<p>Failed to load comments</p>
|
||||
<p>{t('miscUi.failedToLoadComments')}</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
|
@ -45,7 +51,7 @@ export function CommentsSection({
|
|||
<CardHeader className="px-2 pt-6 pb-4 sm:p-6">
|
||||
<CardTitle className="flex items-center space-x-2">
|
||||
<MessageSquare className="h-5 w-5" />
|
||||
<span>{title}</span>
|
||||
<span>{resolvedTitle}</span>
|
||||
{!isLoading && (
|
||||
<span className="text-sm font-normal text-muted-foreground">
|
||||
({comments.length})
|
||||
|
|
@ -80,8 +86,8 @@ export function CommentsSection({
|
|||
) : comments.length === 0 ? (
|
||||
<div className="text-center py-8 text-muted-foreground">
|
||||
<MessageSquare className="h-12 w-12 mx-auto mb-4 opacity-30" />
|
||||
<p className="text-lg font-medium mb-2">{emptyStateMessage}</p>
|
||||
<p className="text-sm">{emptyStateSubtitle}</p>
|
||||
<p className="text-lg font-medium mb-2">{resolvedEmpty}</p>
|
||||
<p className="text-sm">{resolvedEmptySub}</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
|
|
|
|||
|
|
@ -6821,6 +6821,63 @@ walletUi: {
|
|||
itemsReadyToSell: '{{count}} item(s) ready to sell',
|
||||
},
|
||||
},
|
||||
miscUi: {
|
||||
openingCamera: 'Opening camera…',
|
||||
usePhoneCamera: 'Use your phone camera, then return here',
|
||||
startingCamera: 'Starting camera...',
|
||||
cameraAccessRequired: 'Camera Access Required',
|
||||
cameraAccessHint: 'Allow camera access in your browser settings, or use the file picker below.',
|
||||
cameraErrorTitle: 'Camera error',
|
||||
cameraErrorDesc: 'Could not open the camera. Please check app permissions in Settings.',
|
||||
cameraDeniedTitle: 'Camera access denied',
|
||||
cameraDeniedDesc: 'Please allow camera access in your browser settings.',
|
||||
uploadFromGallery: 'Upload from Gallery',
|
||||
uploading: 'Uploading...',
|
||||
usePhoto: 'Use Photo',
|
||||
earningPotential: 'Earning Potential',
|
||||
qualityExcellent: 'Excellent!',
|
||||
qualityGood: 'Good',
|
||||
qualityBasic: 'Basic',
|
||||
qualityGettingStarted: 'Getting started',
|
||||
qualityLocationAdded: 'Location added',
|
||||
qualityOnePhoto: 'At least 1 photo',
|
||||
qualityThreePhotos: '3+ photos',
|
||||
qualityMoreAngles: 'More angles = better',
|
||||
qualityDescription: 'Description',
|
||||
qualityDetailedDesc: 'Detailed description (100+ chars)',
|
||||
qualityContact: 'Contact info',
|
||||
qualityPhoneOrWebsite: 'Phone or website',
|
||||
qualityHours: 'Opening hours',
|
||||
qualitySpotType: 'Spot type selected',
|
||||
failedToLoadComments: 'Failed to load comments',
|
||||
commentsTitle: 'Comments',
|
||||
noCommentsYet: 'No comments yet',
|
||||
beFirstComment: 'Be the first to share your thoughts!',
|
||||
removeMember: 'Remove member',
|
||||
whoIsThisFor: 'Who is this profile for?',
|
||||
individual: 'Individual',
|
||||
individualDesc: 'A single person',
|
||||
collective: 'Collective',
|
||||
collectiveDesc: 'Crew, studio or company',
|
||||
membersLabel: 'Members',
|
||||
entityMembersHint: 'Add the Nostr accounts of your crew. Each member confirms their own spot with their key — until then they show as “pending”.',
|
||||
entityVerifiedHint: 'Membership is verified per member — nobody can be added to your crew without confirming from their own account.',
|
||||
entityInvalidPubkey: 'Enter a valid npub, nprofile, or 64-char hex pubkey.',
|
||||
entityAlreadyAdded: 'That member is already added.',
|
||||
offline: 'Offline',
|
||||
connected: 'Connected',
|
||||
offlineMode: 'Offline Mode',
|
||||
allFeaturesAvailable: 'All features available',
|
||||
browsingCachedSpots: 'Browsing cached spots',
|
||||
queuedActions: '{{count}} queued action(s)',
|
||||
syncing: 'Syncing',
|
||||
publishingWhenStable: 'Publishing when connection stabilizes...',
|
||||
willPublishOnline: 'Will be published when back online',
|
||||
availableOffline: 'Available offline:',
|
||||
offlineBrowseSpots: 'Browse previously viewed spots',
|
||||
offlineViewDetails: 'View cached spot details',
|
||||
offlineKeepActing: 'Keep acting — posts, RSVPs, listings and messages are queued and send when you reconnect',
|
||||
},
|
||||
cardUi: {
|
||||
save: 'Save card to Nostr',
|
||||
saved: 'Card saved',
|
||||
|
|
|
|||
|
|
@ -6821,6 +6821,63 @@ walletUi: {
|
|||
itemsReadyToSell: '{{count}} artículo(s) listos para vender',
|
||||
},
|
||||
},
|
||||
miscUi: {
|
||||
openingCamera: 'Abriendo cámara…',
|
||||
usePhoneCamera: 'Usa la cámara del teléfono y vuelve aquí',
|
||||
startingCamera: 'Iniciando cámara...',
|
||||
cameraAccessRequired: 'Se requiere acceso a la cámara',
|
||||
cameraAccessHint: 'Permite el acceso a la cámara en los ajustes del navegador, o usa el selector de archivos de abajo.',
|
||||
cameraErrorTitle: 'Error de cámara',
|
||||
cameraErrorDesc: 'No se pudo abrir la cámara. Revisa los permisos de la app en Ajustes.',
|
||||
cameraDeniedTitle: 'Acceso a la cámara denegado',
|
||||
cameraDeniedDesc: 'Permite el acceso a la cámara en los ajustes del navegador.',
|
||||
uploadFromGallery: 'Subir desde la galería',
|
||||
uploading: 'Subiendo...',
|
||||
usePhoto: 'Usar foto',
|
||||
earningPotential: 'Potencial de ganancias',
|
||||
qualityExcellent: '¡Excelente!',
|
||||
qualityGood: 'Bueno',
|
||||
qualityBasic: 'Básico',
|
||||
qualityGettingStarted: 'Empezando',
|
||||
qualityLocationAdded: 'Ubicación añadida',
|
||||
qualityOnePhoto: 'Al menos 1 foto',
|
||||
qualityThreePhotos: '3+ fotos',
|
||||
qualityMoreAngles: 'Más ángulos = mejor',
|
||||
qualityDescription: 'Descripción',
|
||||
qualityDetailedDesc: 'Descripción detallada (100+ caracteres)',
|
||||
qualityContact: 'Datos de contacto',
|
||||
qualityPhoneOrWebsite: 'Teléfono o web',
|
||||
qualityHours: 'Horario de apertura',
|
||||
qualitySpotType: 'Tipo de spot elegido',
|
||||
failedToLoadComments: 'No se pudieron cargar los comentarios',
|
||||
commentsTitle: 'Comentarios',
|
||||
noCommentsYet: 'Aún no hay comentarios',
|
||||
beFirstComment: '¡Sé el primero en opinar!',
|
||||
removeMember: 'Quitar miembro',
|
||||
whoIsThisFor: '¿Para quién es este perfil?',
|
||||
individual: 'Individual',
|
||||
individualDesc: 'Una sola persona',
|
||||
collective: 'Colectivo',
|
||||
collectiveDesc: 'Crew, estudio o empresa',
|
||||
membersLabel: 'Miembros',
|
||||
entityMembersHint: 'Añade las cuentas Nostr de tu crew. Cada miembro confirma su propio spot con su clave — hasta entonces aparecen como “pendientes”.',
|
||||
entityVerifiedHint: 'La membresía se verifica por miembro — nadie puede añadirse a tu crew sin confirmarlo desde su propia cuenta.',
|
||||
entityInvalidPubkey: 'Introduce un npub, nprofile o clave hex de 64 caracteres válidos.',
|
||||
entityAlreadyAdded: 'Ese miembro ya está añadido.',
|
||||
offline: 'Sin conexión',
|
||||
connected: 'Conectado',
|
||||
offlineMode: 'Modo sin conexión',
|
||||
allFeaturesAvailable: 'Todas las funciones disponibles',
|
||||
browsingCachedSpots: 'Navegando spots en caché',
|
||||
queuedActions: '{{count}} acción(es) en cola',
|
||||
syncing: 'Sincronizando',
|
||||
publishingWhenStable: 'Publicando cuando se estabilice la conexión...',
|
||||
willPublishOnline: 'Se publicará al volver a estar en línea',
|
||||
availableOffline: 'Disponible sin conexión:',
|
||||
offlineBrowseSpots: 'Explora spots vistos antes',
|
||||
offlineViewDetails: 'Ver detalles de spots en caché',
|
||||
offlineKeepActing: 'Sigue actuando — publicaciones, RSVPs, anuncios y mensajes se ponen en cola y se envían al reconectar',
|
||||
},
|
||||
cardUi: {
|
||||
save: 'Guardar carné en Nostr',
|
||||
saved: 'Carné guardado',
|
||||
|
|
|
|||
Loading…
Reference in a new issue