- Is this spot exposed to the weather? Helps skaters find dry spots when it rains.
+ {t('spotUi.shelterHint')}
{shelterOptions.map((option) => (
@@ -628,14 +630,14 @@ export default function EditSpotPage() {
{/* Freeform Tags */}
-
Tags
+
{t('spotUi.tags')}
- Add tags for features, surfaces, vibes (max {MAX_TAGS})
+ {t('spotUi.tagsHint', { max: MAX_TAGS })}
@@ -644,11 +646,11 @@ export default function EditSpotPage() {
{/* Images */}
-
Photos
+
{t('spots.photos')}
{images.map((url) => (
-
+
removeImage(url)}
@@ -661,8 +663,8 @@ export default function EditSpotPage() {
@@ -677,12 +679,12 @@ export default function EditSpotPage() {
{isPublishing ? (
<>
- Saving...
+ {t('spotUi.saving')}
>
) : (
<>
- Save Changes
+ {t('spotUi.saveChanges')}
>
)}
diff --git a/src/pages/GearSwapPage.tsx b/src/pages/GearSwapPage.tsx
index 9c870b3..04877f5 100644
--- a/src/pages/GearSwapPage.tsx
+++ b/src/pages/GearSwapPage.tsx
@@ -20,11 +20,13 @@ import { GEAR_CATEGORIES } from '@/lib/types';
import { getLastLocation } from '@/lib/lastLocation';
import { itemDistanceKm } from '@/lib/sortOptions';
import { ShoppingCart, Plus, Search, Filter, ArrowUpDown, Users, ShieldCheck, UserCheck, AlertCircle, MapPin } from 'lucide-react';
+import { useTranslation } from '@/contexts/language';
type SortOption = 'newest' | 'price_low' | 'price_high' | 'trust' | 'distance';
type TrustFilter = 'all' | 'verified' | 'trusted' | 'following';
export default function GearSwapPage() {
+ const { t } = useTranslation();
useSeoMeta({
title: 'Gear Swap - SkateSpots',
description: 'Buy & sell used skate gear with other skaters. Decks, trucks, wheels, shoes, and more.',
@@ -180,7 +182,7 @@ export default function GearSwapPage() {
}, [items, searchQuery, selectedCategory, sortBy, trustFilter, showOnlyWTB, showOnlySelling, fulfilmentFilter, trustScores, followingSet]);
const categoryOptions = [
- { value: 'all', label: 'All Categories' },
+ { value: 'all', label: t('marketUi.gearSwap.allCategories') },
...Object.entries(GEAR_CATEGORIES).map(([key, label]) => ({
value: key,
label,
@@ -191,10 +193,10 @@ export default function GearSwapPage() {
<>
@@ -203,7 +205,7 @@ export default function GearSwapPage() {
className="gap-2 bg-orange-500 hover:bg-orange-600 text-white font-bold uppercase shadow-[0_0_20px_rgba(255,102,0,0.3)]"
>
- List Gear
+ {t('marketUi.gearSwap.listGear')}
}
@@ -216,7 +218,7 @@ export default function GearSwapPage() {
setSearchQuery(e.target.value)}
className="pl-10 bg-gray-900 border-gray-700 h-12 text-lg"
@@ -259,7 +261,7 @@ export default function GearSwapPage() {
: 'border-gray-600 text-gray-400 hover:bg-gray-800'
}
>
- For Sale
+ {t('marketUi.gearSwap.forSale')}
- WTB
+ {t('marketUi.gearSwap.wtb')}
@@ -290,7 +292,7 @@ export default function GearSwapPage() {
className="gap-2 border-gray-600 text-gray-300 hover:bg-gray-800"
>
- Trust
+ {t('marketUi.gearSwap.trust')}
{trustFilter !== 'all' && (
{trustFilter}
@@ -299,28 +301,28 @@ export default function GearSwapPage() {
- Filter by Trust
+ {t('marketUi.gearSwap.filterByTrust')}
setTrustFilter('all')}
>
- All Sellers
+ {t('marketUi.gearSwap.allSellers')}
setTrustFilter('verified')}
>
- Verified Only
+ {t('marketUi.gearSwap.verifiedOnly')}
setTrustFilter('trusted')}
>
- Trusted+
+ {t('marketUi.gearSwap.trustedPlus')}
- Following Only
+ {t('marketUi.gearSwap.followingOnly')}
@@ -342,12 +344,12 @@ export default function GearSwapPage() {
>
-
+
- Pickup or ship
- 📍 Local pickup
- 📦 Ships
+ {t('marketUi.gearSwap.pickupOrShip')}
+ 📍 {t('marketUi.gearSwap.localPickup')}
+ 📦 {t('marketUi.gearSwap.ships')}
@@ -358,14 +360,14 @@ export default function GearSwapPage() {
>
-
+
- Newest
- Price: Low → High
- Price: High → Low
- Trust Score
- Nearest (local)
+ {t('marketUi.gearSwap.newest')}
+ {t('marketUi.gearSwap.priceLowHigh')}
+ {t('marketUi.gearSwap.priceHighLow')}
+ {t('marketUi.gearSwap.trustScore')}
+ {t('marketUi.gearSwap.nearest')}
@@ -376,7 +378,9 @@ export default function GearSwapPage() {
{!isLoading && (
- {filteredItems.length} {filteredItems.length === 1 ? 'listing' : 'listings'} found
+ {filteredItems.length === 1
+ ? t('marketUi.gearSwap.listingFound', { count: filteredItems.length })
+ : t('marketUi.gearSwap.listingsFound', { count: filteredItems.length })}
)}
@@ -405,8 +409,8 @@ export default function GearSwapPage() {
- Failed to load listings
- Please try again later.
+ {t('marketUi.gearSwap.loadFailed')}
+ {t('marketUi.gearSwap.tryLater')}
)}
@@ -416,11 +420,11 @@ export default function GearSwapPage() {
- No listings found
+ {t('marketUi.gearSwap.noneTitle')}
{searchQuery || selectedCategory !== 'all' || trustFilter !== 'all'
- ? 'No gear matches your current filters. Try clearing them or searching something else.'
- : 'Be the first to list your gear — decks, trucks, wheels, shoes, anything skate!'}
+ ? t('marketUi.gearSwap.noneFiltered')
+ : t('marketUi.gearSwap.noneFirst')}
{(searchQuery || selectedCategory !== 'all' || trustFilter !== 'all') && (
@@ -434,18 +438,18 @@ export default function GearSwapPage() {
}}
>
- Clear Filters
+ {t('common.clearFilters')}
)}
- List Your Gear
+ {t('marketUi.gearSwap.listYourGear')}
- Have questions? Check the FAQ →
+ {t('marketUi.gearSwap.faqLink')}
diff --git a/src/pages/LabPage.tsx b/src/pages/LabPage.tsx
index be6c396..3eb5f57 100644
--- a/src/pages/LabPage.tsx
+++ b/src/pages/LabPage.tsx
@@ -344,6 +344,7 @@ const ACCENT = {
// ─── FAB — offer services ─────────────────────────────────────────────────────
function OfferFAB() {
+ const { t } = useTranslation();
const [open, setOpen] = useState(false);
const ref = useRef
(null);
@@ -358,20 +359,20 @@ function OfferFAB() {
}, [open]);
const items = [
- { to: '/become/coach', label: 'Offer Coaching', color: 'bg-[#ff0040] hover:bg-[#ff0040]/90', icon: GraduationCap },
- { to: '/become/filmer', label: 'Offer Filming', color: 'bg-orange-500 hover:bg-orange-600', icon: Video },
- { to: '/become/photographer', label: 'Offer Photography', color: 'bg-indigo-600 hover:bg-indigo-700', icon: Camera },
- { to: '/become/editor', label: 'Offer Editing', color: 'bg-purple-600 hover:bg-purple-700', icon: Scissors },
- { to: '/become/builder', label: 'Offer Building', color: 'bg-amber-500 hover:bg-amber-600 text-black', icon: HardHat },
- { to: '/become/artist', label: 'Offer Design / Art', color: 'bg-fuchsia-600 hover:bg-fuchsia-700', icon: Palette },
- { to: '/become/graffiti', label: 'Offer Graffiti', color: 'bg-lime-500 hover:bg-lime-600 text-black', icon: SprayCan },
- { to: '/become/tattoo', label: 'Offer Tattoo', color: 'bg-rose-600 hover:bg-rose-700', icon: Brush },
- { to: '/become/trainer', label: 'Offer Training / Physio', color: 'bg-emerald-600 hover:bg-emerald-700', icon: Dumbbell },
- { to: '/become/musician', label: 'Add Music Profile', color: 'bg-sky-500 hover:bg-sky-600', icon: Music },
- { to: '/become/judge', label: 'Judge Events', color: 'bg-yellow-500 hover:bg-yellow-600 text-black', icon: Gavel },
- { to: '/become/organizer', label: 'Organize Events', color: 'bg-teal-600 hover:bg-teal-700', icon: Tent },
- { to: '/become/barber', label: 'Offer Cuts', color: 'bg-cyan-600 hover:bg-cyan-700', icon: Sparkles },
- { to: '/card', label: 'Get Rider Card', color: 'bg-[#ccff00]/20 hover:bg-[#ccff00]/30 border border-[#ccff00]/50 text-[#ccff00]', icon: CreditCard },
+ { to: '/become/coach', label: t('labUi.offerCoaching'), color: 'bg-[#ff0040] hover:bg-[#ff0040]/90', icon: GraduationCap },
+ { to: '/become/filmer', label: t('labUi.offerFilming'), color: 'bg-orange-500 hover:bg-orange-600', icon: Video },
+ { to: '/become/photographer', label: t('labUi.offerPhotography'), color: 'bg-indigo-600 hover:bg-indigo-700', icon: Camera },
+ { to: '/become/editor', label: t('labUi.offerEditing'), color: 'bg-purple-600 hover:bg-purple-700', icon: Scissors },
+ { to: '/become/builder', label: t('labUi.offerBuilding'), color: 'bg-amber-500 hover:bg-amber-600 text-black', icon: HardHat },
+ { to: '/become/artist', label: t('labUi.offerDesignArt'), color: 'bg-fuchsia-600 hover:bg-fuchsia-700', icon: Palette },
+ { to: '/become/graffiti', label: t('labUi.offerGraffiti'), color: 'bg-lime-500 hover:bg-lime-600 text-black', icon: SprayCan },
+ { to: '/become/tattoo', label: t('labUi.offerTattoo'), color: 'bg-rose-600 hover:bg-rose-700', icon: Brush },
+ { to: '/become/trainer', label: t('labUi.offerTraining'), color: 'bg-emerald-600 hover:bg-emerald-700', icon: Dumbbell },
+ { to: '/become/musician', label: t('labUi.addMusicProfile'), color: 'bg-sky-500 hover:bg-sky-600', icon: Music },
+ { to: '/become/judge', label: t('labUi.judgeEvents'), color: 'bg-yellow-500 hover:bg-yellow-600 text-black', icon: Gavel },
+ { to: '/become/organizer', label: t('labUi.organizeEvents'), color: 'bg-teal-600 hover:bg-teal-700', icon: Tent },
+ { to: '/become/barber', label: t('labUi.offerCuts'), color: 'bg-cyan-600 hover:bg-cyan-700', icon: Sparkles },
+ { to: '/card', label: t('labUi.getRiderCard'), color: 'bg-[#ccff00]/20 hover:bg-[#ccff00]/30 border border-[#ccff00]/50 text-[#ccff00]', icon: CreditCard },
];
return (
@@ -386,8 +387,8 @@ function OfferFAB() {
)}
>
{open ? : }
- Offer Services
- Offer
+ {t('labUi.offerServices')}
+ {t('labUi.offer')}
{/* Dropdown */}
@@ -471,20 +472,21 @@ function LabToolbar({
trustFilter,
onClearAll,
}: ToolbarProps) {
+ const { t } = useTranslation();
const tabs: { key: ServiceType; label: string; icon: React.ComponentType<{ className?: string }> }[] = [
- { key: 'coaching', label: 'Coaches', icon: GraduationCap },
- { key: 'filming', label: 'Filmers', icon: Video },
- { key: 'photography', label: 'Photo', icon: Camera },
- { key: 'editing', label: 'Editors', icon: Scissors },
- { key: 'building', label: 'Builders', icon: HardHat },
- { key: 'art', label: 'Artists', icon: Palette },
- { key: 'graffiti', label: 'Graffiti', icon: SprayCan },
- { key: 'tattoo', label: 'Tattoo', icon: Brush },
- { key: 'training', label: 'Trainers', icon: Dumbbell },
- { key: 'music', label: 'Music', icon: Music },
- { key: 'judging', label: 'Judges', icon: Gavel },
- { key: 'organizing', label: 'Organizers', icon: Tent },
- { key: 'barbering', label: 'Barbers', icon: Sparkles },
+ { key: 'coaching', label: t('labUi.tabCoaches'), icon: GraduationCap },
+ { key: 'filming', label: t('labUi.tabFilmers'), icon: Video },
+ { key: 'photography', label: t('labUi.tabPhoto'), icon: Camera },
+ { key: 'editing', label: t('labUi.tabEditors'), icon: Scissors },
+ { key: 'building', label: t('labUi.tabBuilders'), icon: HardHat },
+ { key: 'art', label: t('labUi.tabArtists'), icon: Palette },
+ { key: 'graffiti', label: t('labUi.tabGraffiti'), icon: SprayCan },
+ { key: 'tattoo', label: t('labUi.tabTattoo'), icon: Brush },
+ { key: 'training', label: t('labUi.tabTrainers'), icon: Dumbbell },
+ { key: 'music', label: t('labUi.tabMusic'), icon: Music },
+ { key: 'judging', label: t('labUi.tabJudges'), icon: Gavel },
+ { key: 'organizing', label: t('labUi.tabOrganizers'), icon: Tent },
+ { key: 'barbering', label: t('labUi.tabBarbers'), icon: Sparkles },
];
const accent = ACCENT[serviceType];
@@ -543,7 +545,7 @@ function LabToolbar({
type="text"
value={searchQuery}
onChange={(e) => onSearchChange(e.target.value)}
- placeholder="Name, bio…"
+ placeholder={t('labUi.searchNamePlaceholder')}
className="w-full h-9 pl-8 pr-3 rounded-xl bg-white/5 border border-white/10 text-sm text-white placeholder:text-gray-600 focus:outline-none focus:border-white/30 transition-all"
/>
{searchQuery && (
@@ -563,7 +565,7 @@ function LabToolbar({
type="text"
value={locationQuery}
onChange={(e) => onLocationChange(e.target.value)}
- placeholder="City…"
+ placeholder={t('labUi.searchCityPlaceholder')}
className="w-full h-9 pl-8 pr-3 rounded-xl bg-white/5 border border-white/10 text-sm text-white placeholder:text-gray-600 focus:outline-none focus:border-white/30 transition-all"
/>
{locationQuery && (
@@ -587,7 +589,7 @@ function LabToolbar({
)}
>
- Filters
+ {t('labUi.filters')}
{activeFilterCount > 0 && (
{activeFilterCount}
@@ -618,7 +620,7 @@ function LabToolbar({
{sortBy !== 'recent' && (
- Top Rated
+ {t('labUi.topRated')}
)}
@@ -626,7 +628,7 @@ function LabToolbar({
{trustFilter !== 'all' && (
- {trustFilter === 'verified' ? 'Verified' : trustFilter === 'trusted' ? 'Trusted' : 'Following'}
+ {trustFilter === 'verified' ? t('common.verified') : trustFilter === 'trusted' ? t('labUi.trusted') : t('labUi.following')}
)}
@@ -635,7 +637,7 @@ function LabToolbar({
onClick={onClearAll}
className="px-2.5 py-1 rounded-full text-[10px] font-bold text-gray-500 hover:text-red-400 border border-white/10 hover:border-red-500/30 transition-all"
>
- Clear all
+ {t('labUi.clearAll')}
)}
@@ -644,8 +646,8 @@ function LabToolbar({
{!isLoading && (
{totalCount === 0
- ? 'No results'
- : `${totalCount} result${totalCount !== 1 ? 's' : ''}`}
+ ? t('labUi.noResults')
+ : t(totalCount === 1 ? 'labUi.resultsOne' : 'labUi.resultsMany', { count: totalCount })}
)}
@@ -681,87 +683,88 @@ function FilterDrawer({
onClearAll,
activeCount,
}: FilterDrawerProps) {
+ const { t } = useTranslation();
const accent = ACCENT[serviceType];
const skillSections: { title: string; entries: [string, string][]; activeColor: string }[] =
serviceType === 'coaching'
- ? [{ title: 'Skills to Learn', entries: COACH_SKILL_ENTRIES, activeColor: accent.chip }]
+ ? [{ title: t('labUi.filterSkillsToLearn'), entries: COACH_SKILL_ENTRIES, activeColor: accent.chip }]
: serviceType === 'filming'
? [
- { title: 'Content Type', entries: FILMING_SERVICE_ENTRIES, activeColor: accent.chip },
- { title: 'Equipment & Style', entries: Object.entries(FILMING_SKILL_LABELS), activeColor: 'bg-orange-400 text-white border-orange-400' },
+ { title: t('labUi.filterContentType'), entries: FILMING_SERVICE_ENTRIES, activeColor: accent.chip },
+ { title: t('labUi.filterEquipmentStyle'), entries: Object.entries(FILMING_SKILL_LABELS), activeColor: 'bg-orange-400 text-white border-orange-400' },
]
: serviceType === 'photography'
? [
- { title: 'Services', entries: PHOTO_SERVICE_ENTRIES, activeColor: accent.chip },
- { title: 'Camera', entries: PHOTO_CAMERA_ENTRIES, activeColor: 'bg-indigo-400 text-white border-indigo-400' },
- { title: 'Lenses', entries: PHOTO_LENS_ENTRIES, activeColor: 'bg-indigo-400 text-white border-indigo-400' },
- { title: 'Lighting', entries: PHOTO_LIGHTING_ENTRIES, activeColor: 'bg-indigo-400 text-white border-indigo-400' },
- { title: 'Support & Rig', entries: PHOTO_SUPPORT_ENTRIES, activeColor: 'bg-indigo-400 text-white border-indigo-400' },
- { title: 'Style', entries: PHOTO_STYLE_ENTRIES, activeColor: 'bg-indigo-400 text-white border-indigo-400' },
+ { title: t('labUi.filterServices'), entries: PHOTO_SERVICE_ENTRIES, activeColor: accent.chip },
+ { title: t('labUi.filterCamera'), entries: PHOTO_CAMERA_ENTRIES, activeColor: 'bg-indigo-400 text-white border-indigo-400' },
+ { title: t('labUi.filterLenses'), entries: PHOTO_LENS_ENTRIES, activeColor: 'bg-indigo-400 text-white border-indigo-400' },
+ { title: t('labUi.filterLighting'), entries: PHOTO_LIGHTING_ENTRIES, activeColor: 'bg-indigo-400 text-white border-indigo-400' },
+ { title: t('labUi.filterSupportRig'), entries: PHOTO_SUPPORT_ENTRIES, activeColor: 'bg-indigo-400 text-white border-indigo-400' },
+ { title: t('labUi.filterStyle'), entries: PHOTO_STYLE_ENTRIES, activeColor: 'bg-indigo-400 text-white border-indigo-400' },
]
: serviceType === 'building'
? [
- { title: 'What They Build', entries: BUILDER_SERVICE_ENTRIES, activeColor: accent.chip },
- { title: 'Materials & Trade', entries: BUILDER_SKILL_ENTRIES, activeColor: 'bg-amber-400 text-black border-amber-400' },
- { title: 'Project Scale', entries: BUILDER_SCALE_ENTRIES, activeColor: 'bg-amber-400 text-black border-amber-400' },
+ { title: t('labUi.filterWhatTheyBuild'), entries: BUILDER_SERVICE_ENTRIES, activeColor: accent.chip },
+ { title: t('labUi.filterMaterialsTrade'), entries: BUILDER_SKILL_ENTRIES, activeColor: 'bg-amber-400 text-black border-amber-400' },
+ { title: t('labUi.filterProjectScale'), entries: BUILDER_SCALE_ENTRIES, activeColor: 'bg-amber-400 text-black border-amber-400' },
]
: serviceType === 'art'
? [
- { title: 'What They Make', entries: ARTIST_SERVICE_ENTRIES, activeColor: accent.chip },
- { title: 'Medium', entries: ARTIST_MEDIUM_ENTRIES, activeColor: 'bg-fuchsia-400 text-white border-fuchsia-400' },
- { title: 'Software & Tools', entries: ARTIST_TOOL_ENTRIES, activeColor: 'bg-fuchsia-400 text-white border-fuchsia-400' },
+ { title: t('labUi.filterWhatTheyMake'), entries: ARTIST_SERVICE_ENTRIES, activeColor: accent.chip },
+ { title: t('labUi.filterMedium'), entries: ARTIST_MEDIUM_ENTRIES, activeColor: 'bg-fuchsia-400 text-white border-fuchsia-400' },
+ { title: t('labUi.filterSoftwareTools'), entries: ARTIST_TOOL_ENTRIES, activeColor: 'bg-fuchsia-400 text-white border-fuchsia-400' },
]
: serviceType === 'graffiti'
? [
- { title: 'Style', entries: GRAFFITI_STYLE_ENTRIES, activeColor: accent.chip },
- { title: 'Medium', entries: GRAFFITI_MEDIUM_ENTRIES, activeColor: 'bg-lime-400 text-black border-lime-400' },
- { title: 'Work Context', entries: GRAFFITI_CONTEXT_ENTRIES, activeColor: 'bg-lime-400 text-black border-lime-400' },
+ { title: t('labUi.filterStyle'), entries: GRAFFITI_STYLE_ENTRIES, activeColor: accent.chip },
+ { title: t('labUi.filterMedium'), entries: GRAFFITI_MEDIUM_ENTRIES, activeColor: 'bg-lime-400 text-black border-lime-400' },
+ { title: t('labUi.filterWorkContext'), entries: GRAFFITI_CONTEXT_ENTRIES, activeColor: 'bg-lime-400 text-black border-lime-400' },
]
: serviceType === 'tattoo'
? [
- { title: 'Style', entries: TATTOO_STYLE_ENTRIES, activeColor: accent.chip },
- { title: 'Method', entries: TATTOO_METHOD_ENTRIES, activeColor: 'bg-rose-400 text-white border-rose-400' },
+ { title: t('labUi.filterStyle'), entries: TATTOO_STYLE_ENTRIES, activeColor: accent.chip },
+ { title: t('labUi.filterMethod'), entries: TATTOO_METHOD_ENTRIES, activeColor: 'bg-rose-400 text-white border-rose-400' },
]
: serviceType === 'training'
? [
- { title: 'Focus', entries: TRAINER_FOCUS_ENTRIES, activeColor: accent.chip },
- { title: 'Injuries Treated', entries: TRAINER_INJURY_ENTRIES, activeColor: 'bg-emerald-400 text-black border-emerald-400' },
- { title: 'Credentials & Setting', entries: TRAINER_CREDENTIAL_ENTRIES, activeColor: 'bg-emerald-400 text-black border-emerald-400' },
+ { title: t('labUi.filterFocus'), entries: TRAINER_FOCUS_ENTRIES, activeColor: accent.chip },
+ { title: t('labUi.filterInjuriesTreated'), entries: TRAINER_INJURY_ENTRIES, activeColor: 'bg-emerald-400 text-black border-emerald-400' },
+ { title: t('labUi.filterCredentialsSetting'), entries: TRAINER_CREDENTIAL_ENTRIES, activeColor: 'bg-emerald-400 text-black border-emerald-400' },
]
: serviceType === 'music'
? [
- { title: 'Genre', entries: MUSICIAN_GENRE_ENTRIES, activeColor: accent.chip },
- { title: 'Instruments & Role', entries: MUSICIAN_INSTRUMENT_ENTRIES, activeColor: 'bg-sky-400 text-white border-sky-400' },
+ { title: t('labUi.filterGenre'), entries: MUSICIAN_GENRE_ENTRIES, activeColor: accent.chip },
+ { title: t('labUi.filterInstrumentsRole'), entries: MUSICIAN_INSTRUMENT_ENTRIES, activeColor: 'bg-sky-400 text-white border-sky-400' },
]
: serviceType === 'judging'
? [
- { title: 'Event Formats', entries: JUDGE_FORMAT_ENTRIES, activeColor: accent.chip },
- { title: 'Specialty', entries: JUDGE_SPECIALTY_ENTRIES, activeColor: 'bg-yellow-400 text-black border-yellow-400' },
- { title: 'Level / Circuit', entries: JUDGE_LEVEL_ENTRIES, activeColor: 'bg-yellow-400 text-black border-yellow-400' },
+ { title: t('labUi.filterEventFormats'), entries: JUDGE_FORMAT_ENTRIES, activeColor: accent.chip },
+ { title: t('labUi.filterSpecialty'), entries: JUDGE_SPECIALTY_ENTRIES, activeColor: 'bg-yellow-400 text-black border-yellow-400' },
+ { title: t('labUi.filterLevelCircuit'), entries: JUDGE_LEVEL_ENTRIES, activeColor: 'bg-yellow-400 text-black border-yellow-400' },
]
: serviceType === 'organizing'
? [
- { title: 'Event Types', entries: ORGANIZER_EVENT_ENTRIES, activeColor: accent.chip },
- { title: 'What You Handle', entries: ORGANIZER_SERVICE_ENTRIES, activeColor: 'bg-teal-400 text-black border-teal-400' },
- { title: 'Scale', entries: ORGANIZER_SCALE_ENTRIES, activeColor: 'bg-teal-400 text-black border-teal-400' },
+ { title: t('labUi.filterEventTypes'), entries: ORGANIZER_EVENT_ENTRIES, activeColor: accent.chip },
+ { title: t('labUi.filterWhatYouHandle'), entries: ORGANIZER_SERVICE_ENTRIES, activeColor: 'bg-teal-400 text-black border-teal-400' },
+ { title: t('labUi.filterScale'), entries: ORGANIZER_SCALE_ENTRIES, activeColor: 'bg-teal-400 text-black border-teal-400' },
]
: serviceType === 'barbering'
? [
- { title: 'What They Offer', entries: BARBER_SERVICE_ENTRIES, activeColor: accent.chip },
- { title: 'Signature Styles', entries: BARBER_STYLE_ENTRIES, activeColor: 'bg-cyan-400 text-white border-cyan-400' },
- { title: 'Setting', entries: BARBER_SETTING_ENTRIES, activeColor: 'bg-cyan-400 text-white border-cyan-400' },
+ { title: t('labUi.filterWhatTheyOffer'), entries: BARBER_SERVICE_ENTRIES, activeColor: accent.chip },
+ { title: t('labUi.filterSignatureStyles'), entries: BARBER_STYLE_ENTRIES, activeColor: 'bg-cyan-400 text-white border-cyan-400' },
+ { title: t('labUi.filterSetting'), entries: BARBER_SETTING_ENTRIES, activeColor: 'bg-cyan-400 text-white border-cyan-400' },
]
: [
- { title: 'Edit Type', entries: EDITOR_SERVICE_ENTRIES, activeColor: accent.chip },
- { title: 'Software & Tools', entries: EDITOR_SKILL_ENTRIES, activeColor: 'bg-purple-400 text-white border-purple-400' },
+ { title: t('labUi.filterEditType'), entries: EDITOR_SERVICE_ENTRIES, activeColor: accent.chip },
+ { title: t('labUi.filterSoftwareTools'), entries: EDITOR_SKILL_ENTRIES, activeColor: 'bg-purple-400 text-white border-purple-400' },
];
const trustOptions: { value: TrustFilterLevel; label: string; icon: React.ComponentType<{ className?: string }> }[] = [
- { value: 'all', label: 'All', icon: Users },
- { value: 'verified', label: 'Verified', icon: CheckCircle2 },
- { value: 'trusted', label: 'Trusted', icon: Shield },
- { value: 'following', label: 'Following', icon: CheckCircle2 },
+ { value: 'all', label: t('labUi.all'), icon: Users },
+ { value: 'verified', label: t('common.verified'), icon: CheckCircle2 },
+ { value: 'trusted', label: t('labUi.trusted'), icon: Shield },
+ { value: 'following', label: t('labUi.following'), icon: CheckCircle2 },
];
const drawerContent = (
@@ -770,10 +773,10 @@ function FilterDrawer({
- Filters
+ {t('labUi.filters')}
{activeCount > 0 && (
- {activeCount} active
+ {t('labUi.activeCount', { count: activeCount })}
)}
@@ -783,7 +786,7 @@ function FilterDrawer({
onClick={onClearAll}
className="text-xs text-gray-400 hover:text-red-400 transition-colors font-semibold"
>
- Clear all
+ {t('labUi.clearAll')}
)}
{/* Sort */}
-
Sort By
+
{t('labUi.sortBy')}
{[
- { value: 'recent' as SortOption, label: 'Newest', icon: Clock },
- { value: 'trust' as SortOption, label: 'Top Rated', icon: Star },
+ { value: 'recent' as SortOption, label: t('labUi.newest'), icon: Clock },
+ { value: 'trust' as SortOption, label: t('labUi.topRated'), icon: Star },
].map(({ value, label, icon: Icon }) => (
- Trust Level
+ {t('labUi.trustLevel')}
{trustOptions.map(({ value, label, icon: Icon }) => (
- Show Results{activeCount > 0 ? ` · ${activeCount} active` : ''}
+ {activeCount > 0 ? t('labUi.showResultsActive', { count: activeCount }) : t('labUi.showResults')}
>
@@ -893,7 +896,7 @@ function FilterDrawer({
!v && onClose()}>
- Filters
+ {t('labUi.filters')}
{drawerContent}
@@ -905,7 +908,7 @@ function FilterDrawer({
!v && onClose()}>
- Filters
+ {t('labUi.filters')}
{drawerContent}
@@ -918,6 +921,7 @@ function FilterDrawer({
// ─── Profile Card ─────────────────────────────────────────────────────────────
function ProfileCard({ profile }: { profile: LabProfile }) {
+ const { t } = useTranslation();
const author = useAuthor(profile.pubkey);
const metadata = author.data?.metadata;
const displayName = metadata?.name ?? genUserName(profile.pubkey);
@@ -984,7 +988,7 @@ function ProfileCard({ profile }: { profile: LabProfile }) {
{profile.entityType === 'collective' && (
{profile.members.length > 0 ? profile.members.length : ''}
@@ -1014,7 +1018,7 @@ function ProfileCard({ profile }: { profile: LabProfile }) {
)}
{profile.experience && (
- {profile.experience}yr exp
+ {t('labUi.yearsExp', { count: profile.experience })}
)}
@@ -1072,108 +1076,109 @@ function SkeletonCard() {
// ─── Empty state per-category ─────────────────────────────────────────────────
function EmptyState({ serviceType, user }: { serviceType: ServiceType; user: boolean }) {
+ const { t } = useTranslation();
const configs = {
coaching: {
icon: GraduationCap,
- title: 'No coaches yet',
- body: 'Be the first to offer skateboarding coaching in your area.',
- cta: 'Offer Coaching',
+ title: t('labUi.empty.coachesTitle'),
+ body: t('labUi.empty.coachesBody'),
+ cta: t('labUi.offerCoaching'),
to: '/become/coach',
color: 'bg-[#ff0040] hover:bg-[#ff0040]/90',
},
filming: {
icon: Video,
- title: 'No filmers yet',
- body: 'Be the first to offer skate filming — clips, full parts, contest coverage and more.',
- cta: 'Offer Filming',
+ title: t('labUi.empty.filmersTitle'),
+ body: t('labUi.empty.filmersBody'),
+ cta: t('labUi.offerFilming'),
to: '/become/filmer',
color: 'bg-orange-500 hover:bg-orange-600',
},
photography: {
icon: Camera,
- title: 'No photographers yet',
- body: 'Be the first to offer skate photography in your area — action shots, sequences, editorial and more.',
- cta: 'Offer Photography',
+ title: t('labUi.empty.photographersTitle'),
+ body: t('labUi.empty.photographersBody'),
+ cta: t('labUi.offerPhotography'),
to: '/become/photographer',
color: 'bg-indigo-600 hover:bg-indigo-700',
},
editing: {
icon: Scissors,
- title: 'No video editors yet',
- body: 'Be the first to offer skate video editing services.',
- cta: 'Offer Editing',
+ title: t('labUi.empty.editorsTitle'),
+ body: t('labUi.empty.editorsBody'),
+ cta: t('labUi.offerEditing'),
to: '/become/editor',
color: 'bg-purple-600 hover:bg-purple-700',
},
building: {
icon: HardHat,
- title: 'No builders yet',
- body: 'Be the first to list your ramp, park or DIY building — concrete, wood, rails and more.',
- cta: 'Offer Building',
+ title: t('labUi.empty.buildersTitle'),
+ body: t('labUi.empty.buildersBody'),
+ cta: t('labUi.offerBuilding'),
to: '/become/builder',
color: 'bg-amber-500 hover:bg-amber-600 text-black',
},
art: {
icon: Palette,
- title: 'No artists yet',
- body: 'Be the first to offer board graphics, logos, posters and merch for the scene.',
- cta: 'Offer Design / Art',
+ title: t('labUi.empty.artistsTitle'),
+ body: t('labUi.empty.artistsBody'),
+ cta: t('labUi.offerDesignArt'),
to: '/become/artist',
color: 'bg-fuchsia-600 hover:bg-fuchsia-700',
},
graffiti: {
icon: SprayCan,
- title: 'No graffiti artists yet',
- body: 'Be the first to offer pieces, murals and skatepark art in your area.',
- cta: 'Offer Graffiti',
+ title: t('labUi.empty.graffitiTitle'),
+ body: t('labUi.empty.graffitiBody'),
+ cta: t('labUi.offerGraffiti'),
to: '/become/graffiti',
color: 'bg-lime-500 hover:bg-lime-600 text-black',
},
tattoo: {
icon: Brush,
- title: 'No tattoo artists yet',
- body: 'Be the first skate-culture tattoo artist to list — flash, custom, fine line and more.',
- cta: 'Offer Tattoo',
+ title: t('labUi.empty.tattooTitle'),
+ body: t('labUi.empty.tattooBody'),
+ cta: t('labUi.offerTattoo'),
to: '/become/tattoo',
color: 'bg-rose-600 hover:bg-rose-700',
},
training: {
icon: Dumbbell,
- title: 'No trainers yet',
- body: 'Be the first trainer or physio to help skaters get strong and stay healthy.',
- cta: 'Offer Training / Physio',
+ title: t('labUi.empty.trainersTitle'),
+ body: t('labUi.empty.trainersBody'),
+ cta: t('labUi.offerTraining'),
to: '/become/trainer',
color: 'bg-emerald-600 hover:bg-emerald-700',
},
music: {
icon: Music,
- title: 'No musicians yet',
- body: 'Skate and make music? Add your band or solo project — genres, instruments and links.',
- cta: 'Add Music Profile',
+ title: t('labUi.empty.musiciansTitle'),
+ body: t('labUi.empty.musiciansBody'),
+ cta: t('labUi.addMusicProfile'),
to: '/become/musician',
color: 'bg-sky-500 hover:bg-sky-600',
},
judging: {
icon: Gavel,
- title: 'No judges yet',
- body: 'Run a fair contest? List yourself to judge Game of S.K.A.T.E., best trick jams, contests and touring stops.',
- cta: 'Judge Events',
+ title: t('labUi.empty.judgesTitle'),
+ body: t('labUi.empty.judgesBody'),
+ cta: t('labUi.judgeEvents'),
to: '/become/judge',
color: 'bg-yellow-500 hover:bg-yellow-600 text-black',
},
organizing: {
icon: Tent,
- title: 'No organizers yet',
- body: 'Put on jams, comps, meetups or demos? List yourself so skaters can find and book you.',
- cta: 'Organize Events',
+ title: t('labUi.empty.organizersTitle'),
+ body: t('labUi.empty.organizersBody'),
+ cta: t('labUi.organizeEvents'),
to: '/become/organizer',
color: 'bg-teal-600 hover:bg-teal-700',
},
barbering: {
icon: Sparkles,
- title: 'No barbers yet',
- body: 'Cut hair for skaters? List your chair or mobile service so the scene can get a fresh trim.',
- cta: 'Offer Cuts',
+ title: t('labUi.empty.barbersTitle'),
+ body: t('labUi.empty.barbersBody'),
+ cta: t('labUi.offerCuts'),
to: '/become/barber',
color: 'bg-cyan-600 hover:bg-cyan-700',
},
@@ -1193,9 +1198,9 @@ function EmptyState({ serviceType, user }: { serviceType: ServiceType; user: boo
<>
{c.body}{' '}
- Log in
+ {t('auth.login')}
{' '}
- to create a profile.
+ {t('labUi.empty.loginSuffix')}
>
)
}
@@ -1204,7 +1209,7 @@ function EmptyState({ serviceType, user }: { serviceType: ServiceType; user: boo
? [{ label: c.cta, to: c.to, icon: Plus, className: c.color }]
: []
}
- footer={{ label: 'Have questions? Check the FAQ →', to: '/faq' }}
+ footer={{ label: t('labUi.empty.footer'), to: '/faq' }}
/>
);
}
@@ -1379,7 +1384,7 @@ export default function LabPage() {
) : (
- 🔒 Login to offer services
+ 🔒 {t('labUi.loginToOffer')}
)}
@@ -1401,13 +1406,13 @@ export default function LabPage() {
- The Crew
+ {t('labUi.theCrew')}
- Just a skater ?
+ {t('labUi.justASkater')} {t('labUi.skaterWord')} ?
- The Lab's for services. Browse & join the whole skater community
+ {t('labUi.labForServices')}
diff --git a/src/pages/ListingDetailPage.tsx b/src/pages/ListingDetailPage.tsx
index 2b97c93..479ceb4 100644
--- a/src/pages/ListingDetailPage.tsx
+++ b/src/pages/ListingDetailPage.tsx
@@ -36,6 +36,7 @@ import {
Handshake,
Share2,
} from 'lucide-react';
+import { useTranslation } from '@/contexts/language';
/**
* Full marketplace listing page (`/listing/:pubkey/:identifier`).
@@ -45,6 +46,7 @@ import {
* actions (`ListingTradeActions`, also used by the grid card).
*/
export default function ListingDetailPage() {
+ const { t } = useTranslation();
const { pubkey, identifier } = useParams<{ pubkey: string; identifier: string }>();
const navigate = useNavigate();
const { user } = useCurrentUser();
@@ -84,10 +86,10 @@ export default function ListingDetailPage() {
<>
-
Listing not found
-
It may have been deleted, or the link is wrong.
+
{t('marketUi.listingDetail.notFound')}
+
{t('marketUi.listingDetail.notFoundBody')}
- Back to Gear Swap
+ {t('marketUi.listingDetail.backToGearSwap')}
>
@@ -102,7 +104,7 @@ export default function ListingDetailPage() {
: formatPrice(undefined, item.priceType);
const conditionInfo = item.condition ? GEAR_CONDITIONS[item.condition] : null;
const categoryLabel = item.category.find((c) => GEAR_CATEGORIES[c as keyof typeof GEAR_CATEGORIES]);
- const displayName = metadata?.name ?? (pubkey ? genUserName(pubkey) : 'Unknown');
+ const displayName = metadata?.name ?? (pubkey ? genUserName(pubkey) : t('marketUi.listingDetail.unknown'));
const npub = pubkey ? nip19.npubEncode(pubkey) : '';
const lightboxImages: LightboxImage[] = item.images.map((url) => ({ url, isOwner: isOwnListing }));
@@ -110,20 +112,20 @@ export default function ListingDetailPage() {
const handleShare = async () => {
try {
await navigator.clipboard.writeText(window.location.href);
- toast({ title: 'Link copied', description: 'Share it with a skater.' });
+ toast({ title: t('marketUi.listingDetail.linkCopied'), description: t('marketUi.listingDetail.linkCopiedBody') });
} catch {
- toast({ title: 'Could not copy the link', variant: 'destructive' });
+ toast({ title: t('marketUi.listingDetail.copyFailed'), variant: 'destructive' });
}
};
const handleRelist = async () => {
try {
await relist(item.event);
- toast({ title: 'Relisted', description: 'Your listing is active again.' });
+ toast({ title: t('marketUi.listingDetail.relisted'), description: t('marketUi.listingDetail.relistedBody') });
} catch (error) {
toast({
- title: 'Could not update',
- description: error instanceof Error ? error.message : 'Something went wrong',
+ title: t('marketUi.listingDetail.couldNotUpdate'),
+ description: error instanceof Error ? error.message : t('marketUi.listingDetail.somethingWrong'),
variant: 'destructive',
});
}
@@ -136,7 +138,7 @@ export default function ListingDetailPage() {
onClick={() => navigate('/marketplace')}
className="mb-4 inline-flex items-center gap-1.5 text-sm text-gray-400 hover:text-white transition-colors"
>
-
Gear Swap
+
{t('marketUi.listingDetail.gearSwap')}
@@ -154,11 +156,11 @@ export default function ListingDetailPage() {
'group relative overflow-hidden rounded-2xl border border-white/10 bg-black/40',
i === 0 && item.images.length > 1 ? 'sm:col-span-2 aspect-video' : 'aspect-square',
)}
- aria-label={`View photo ${i + 1}`}
+ aria-label={t('marketUi.listingDetail.viewPhoto', { n: i + 1 })}
>
@@ -171,12 +173,12 @@ export default function ListingDetailPage() {
{isWTB && (
- WANT TO BUY
+ {t('marketUi.listingDetail.wantToBuy')}
)}
{isSold && (
- SOLD
+ {t('marketUi.listingDetail.sold')}
)}
{item.status && item.status !== 'active' && !isSold && (
@@ -215,7 +217,7 @@ export default function ListingDetailPage() {
- Description
+ {t('marketUi.listingDetail.description')}
@@ -236,12 +238,12 @@ export default function ListingDetailPage() {
)}
{item.availability && (
- Usually here {item.availability}
+ {t('marketUi.listingDetail.usuallyHere', { availability: item.availability })}
)}
- Listed{' '}
- {new Date(item.createdAt * 1000).toLocaleDateString()}
+ {' '}
+ {t('marketUi.listingDetail.listed', { date: new Date(item.createdAt * 1000).toLocaleDateString() })}
@@ -272,8 +274,9 @@ export default function ListingDetailPage() {
{sellerInfo && sellerInfo.listingCount > 0 && (
- {sellerInfo.listingCount} listing{sellerInfo.listingCount === 1 ? '' : 's'} ·{' '}
- {sellerInfo.accountAge}d old
+ {sellerInfo.listingCount === 1
+ ? t('marketUi.listingDetail.listingCountOne', { count: sellerInfo.listingCount, age: sellerInfo.accountAge })
+ : t('marketUi.listingDetail.listingCountMany', { count: sellerInfo.listingCount, age: sellerInfo.accountAge })}
)}
@@ -291,7 +294,7 @@ export default function ListingDetailPage() {
disabled={isUpdatingStatus}
className="w-full gap-2 border-white/15"
>
-
Relist as active
+
{t('marketUi.listingDetail.relistActive')}
) : (
setShowMarkSold(true)}
className="w-full gap-2 bg-[#ccff00] font-bold text-black hover:bg-[#ccff00]/90"
>
- Mark as sold
+ {t('marketUi.listingDetail.markSold')}
)}
setShowDelete(true)}
className="w-full gap-2 border-red-500/30 text-red-400 hover:bg-red-500/10"
>
- Delete listing
+ {t('marketUi.listingDetail.deleteListing')}
@@ -322,7 +325,7 @@ export default function ListingDetailPage() {
onClick={() => setShowOffer(true)}
className="mt-2 w-full gap-2 border-orange-500/40 text-orange-400 hover:bg-orange-500/10"
>
-
Make an offer
+
{t('marketUi.listingDetail.makeOffer')}
- Share listing
+ {t('marketUi.listingDetail.shareListing')}
diff --git a/src/pages/MapPage.tsx b/src/pages/MapPage.tsx
index 794c7ca..57507c6 100644
--- a/src/pages/MapPage.tsx
+++ b/src/pages/MapPage.tsx
@@ -94,6 +94,7 @@ type ViewMode = 'map' | 'list';
// Compact stash item card for the drawer
function MiniStashCard({ item, onDelete }: { item: StashItem; onDelete: () => void }) {
+ const { t } = useTranslation();
const category = STASH_CATEGORIES[item.category];
const condition = CONDITION_LABELS[item.condition];
const rarity = (item.rarity || category.rarity) as keyof typeof RARITY_TIERS;
@@ -116,7 +117,7 @@ function MiniStashCard({ item, onDelete }: { item: StashItem; onDelete: () => vo
{isWishlist && (
- Wishlist
+ {t('mapUi.wishlist')}
)}
@@ -179,6 +180,7 @@ function MiniStashCard({ item, onDelete }: { item: StashItem; onDelete: () => vo
// Stash drawer component
function StashDrawer() {
+ const { t } = useTranslation();
const { user } = useCurrentUser();
const { items, stats, isLoading, removeItem, isEncryptionAvailable } = useStash();
const author = useAuthor(user?.pubkey || '');
@@ -195,9 +197,9 @@ function StashDrawer() {
return (
-
Your Private Stash
+
{t('mapUi.yourPrivateStash')}
- Track your gear, set prices, and find trades with other skaters.
+ {t('mapUi.stashIntro')}
@@ -209,12 +211,12 @@ function StashDrawer() {
return (
-
Encryption Required
+
{t('mapUi.encryptionRequired')}
- Use a signer with NIP-44 support (Alby, nos2x, Amber).
+ {t('mapUi.encryptionRequiredDesc')}
-
Learn More
+
{t('common.learnMore')}
);
@@ -233,7 +235,7 @@ function StashDrawer() {
{displayName}
-
{stats.total} items
+
{t('mapUi.itemsCount', { count: stats.total })}
@@ -243,7 +245,7 @@ function StashDrawer() {
- {usdValue ? `~$${usdValue.toFixed(0)}` : 'sats'}
+ {usdValue ? `~$${usdValue.toFixed(0)}` : t('common.sats')}
@@ -253,15 +255,15 @@ function StashDrawer() {
{stashItems.length}
-
Items
+
{t('mapUi.items')}
{wishlistItems.length}
-
Wishlist
+
{t('mapUi.wishlist')}
{Object.keys(stats.byCategory).length}
-
Types
+
{t('mapUi.types')}
@@ -274,11 +276,11 @@ function StashDrawer() {
) : items.length === 0 ? (
-
Your stash is empty
+
{t('mapUi.stashEmpty')}
- Add First Item
+ {t('mapUi.addFirstItem')}
@@ -289,7 +291,7 @@ function StashDrawer() {
- Wishlist ({wishlistItems.length})
+ {t('mapUi.wishlistCount', { count: wishlistItems.length })}
{wishlistItems.slice(0, 3).map(item => (
- My Gear ({stashItems.length})
+ {t('mapUi.myGearCount', { count: stashItems.length })}
{stashItems.slice(0, 5).map(item => (
@@ -328,20 +330,20 @@ function StashDrawer() {
- Open Full Stash
+ {t('mapUi.openFullStash')}
- Add Item
+ {t('mapUi.addItem')}
- Wishlist
+ {t('mapUi.wishlist')}
@@ -369,6 +371,7 @@ function DirectionsButton({
*/
onDirections?: (target: { lat: number; lng: number; label: string }) => void;
}) {
+ const { t } = useTranslation();
const [open, setOpen] = useState(false);
const destination = useMemo(() => {
const la = parseFloat(lat);
@@ -389,7 +392,7 @@ function DirectionsButton({
disabled={!destination}
>
- Directions
+ {t('mapUi.directions')}
{!onDirections && (
void;
onDirections: (target: { lat: number; lng: number; label: string }) => void;
}) {
+ const { t } = useTranslation();
const navigate = useNavigate();
const author = useAuthor(spot.pubkey);
const metadata = author.data?.metadata;
@@ -496,7 +500,7 @@ function SpotBottomSheet({
{displayName.slice(0, 2).toUpperCase()}
- Added by {displayName}
+ {t('mapUi.addedBy')} {displayName}
{/* CTA buttons */}
@@ -506,7 +510,7 @@ function SpotBottomSheet({
className="h-12 font-bold text-sm bg-gradient-to-r from-[#ff0040] to-[#ff6600] hover:opacity-90 rounded-xl"
>
- View Details
+ {t('mapUi.viewDetails')}
{spot.geohash && (() => {
const { latitude, longitude } = ngeohash.decode(spot.geohash);
@@ -700,10 +704,10 @@ export default function MapPage() {
// Getting a GPS lock (especially first-time / on ROMs without Google's
// fused provider) can take a while — let the user know it's working.
const locatingToast = toast({
- title: cached ? 'Refining your location…' : 'Locating you…',
+ title: cached ? t('mapUi.refiningLocation') : t('mapUi.locatingYou'),
description: cached
- ? 'Centered on your last spot — dialing in a fresh GPS fix.'
- : 'Getting a GPS fix. This can take up to a minute the first time.',
+ ? t('mapUi.refiningLocationDesc')
+ : t('mapUi.locatingDesc'),
duration: 60000,
});
@@ -715,8 +719,8 @@ export default function MapPage() {
setNearMeLoading(false);
setNearMeError(null);
toast({
- title: 'Found you',
- description: 'Centered the map on your location.',
+ title: t('mapUi.foundYou'),
+ description: t('mapUi.foundYouDesc'),
duration: 3000,
});
} catch (err) {
@@ -735,9 +739,9 @@ export default function MapPage() {
if (isNativeGeolocation()) {
// Native app: it's an OS permission, not a browser setting.
toast({
- title: 'Location permission needed',
+ title: t('mapUi.locationPermissionNeeded'),
description:
- 'Allow location for SkateSpots in your device Settings → Apps → SkateSpots → Permissions, then tap Near Me again.' +
+ t('mapUi.permissionNeededDesc') +
diag,
variant: 'destructive',
duration: 12000,
@@ -749,22 +753,22 @@ export default function MapPage() {
} else if (kind === 'timeout') {
setNearMeError('timeout');
toast({
- title: 'Location timed out',
+ title: t('mapUi.locationTimedOut'),
description:
(isNativeGeolocation()
- ? "Couldn't get a location fix in time. If you're indoors, GPS can't see satellites — enable Network Location (Settings → Location) or step outside, then try again."
- : 'Could not get your location in time. Check your signal and try again.') + diag,
+ ? t('mapUi.timeoutNativeDesc')
+ : t('mapUi.timeoutWebDesc')) + diag,
variant: 'destructive',
duration: 12000,
});
} else if (kind === 'no_provider') {
setNearMeError('unavailable');
toast({
- title: 'No location provider',
+ title: t('mapUi.noLocationProvider'),
description:
(isNativeGeolocation()
- ? "Your phone isn't providing a location. On privacy-focused ROMs (e.g. GrapheneOS) enable Network Location under Settings → Location, or make sure GPS is on and try outdoors."
- : 'No location provider is available. Enable location services and try again.') +
+ ? t('mapUi.noProviderNativeDesc')
+ : t('mapUi.noProviderWebDesc')) +
diag,
variant: 'destructive',
duration: 14000,
@@ -772,11 +776,11 @@ export default function MapPage() {
} else {
setNearMeError('unavailable');
toast({
- title: 'Location unavailable',
+ title: t('mapUi.locationUnavailable'),
description:
(isNativeGeolocation()
- ? 'Turn on Location/GPS in your phone settings, allow location for SkateSpots (Settings → Apps → SkateSpots → Permissions), then try again.'
- : 'Your location could not be determined. Make sure location services are enabled.') +
+ ? t('mapUi.unavailableNativeDesc')
+ : t('mapUi.unavailableWebDesc')) +
diag,
variant: 'destructive',
duration: 12000,
@@ -1060,8 +1064,8 @@ export default function MapPage() {
🛹
-
New here?
-
Discover skate spots, events & the community.
+
{t('mapUi.newHere')}
+
{t('mapUi.welcomeDesc')}
{/* Desktop-only: grab the mobile app via Zapstore */}
- Get on Zapstore
+ {t('mapUi.getOnZapstore')}
- Get Started
+ {t('common.getStarted')}
setWelcomeDismissed(true)}
className="flex-shrink-0 w-7 h-7 flex items-center justify-center rounded-full hover:bg-white/10 text-white/40 hover:text-white transition-colors"
- aria-label="Dismiss"
+ aria-label={t('mapUi.dismiss')}
>
@@ -1127,7 +1131,7 @@ export default function MapPage() {
className={cn('h-8 w-8 rounded-full flex items-center justify-center transition-all touch-manipulation active:scale-90',
viewMode === 'map' ? 'bg-[#ff0040] shadow-lg shadow-[#ff0040]/30' : 'text-gray-500 hover:text-gray-300')}
onClick={() => setViewMode('map')}
- aria-label="Map view"
+ aria-label={t('mapUi.mapViewAria')}
>
@@ -1135,7 +1139,7 @@ export default function MapPage() {
className={cn('h-8 w-8 rounded-full flex items-center justify-center transition-all touch-manipulation active:scale-90',
viewMode === 'list' ? 'bg-[#ff0040] shadow-lg shadow-[#ff0040]/30' : 'text-gray-500 hover:text-gray-300')}
onClick={() => setViewMode('list')}
- aria-label="List view"
+ aria-label={t('mapUi.listViewAria')}
>
@@ -1151,7 +1155,7 @@ export default function MapPage() {
? 'bg-[#ff0040]/25 border-[#ff0040]/60 text-[#ff0040]'
: 'bg-black/50 backdrop-blur-md border-white/15 text-gray-300 md:bg-white/5 md:border-white/10 md:text-gray-400 hover:text-gray-200 hover:border-white/20'
)}
- aria-label="Filters"
+ aria-label={t('mapPage.filters')}
>
{totalActiveFilters > 0 && (
@@ -1164,7 +1168,7 @@ export default function MapPage() {
- Filters
+ {t('mapPage.filters')}
{hasActiveFilters && (
- Clear
+ {t('common.clear')}
)}
@@ -1184,7 +1188,7 @@ export default function MapPage() {
{/* Layer toggles */}
-
Show on Map
+
{t('mapUi.showOnMap')}
setShowSpots(!showSpots)}
>
- 🛹 Spots
+ 🛹 {t('mapUi.spots')}
setShowShops(!showShops)}
>
- 🏪 Shops
+ 🏪 {t('mapUi.shops')}
setShowGear(!showGear)}
>
- 🛒 Gear
+ 🛒 {t('mapUi.gear')}
setShowTours(!showTours)}
>
- 🗺️ Tours
+ 🗺️ {t('mapUi.tours')}
setShowEvents(!showEvents)}
>
- 📅 Events
+ 📅 {t('mapUi.events')}
setShowCrews(!showCrews)}
>
- 🤘 Crews
+ 🤘 {t('mapUi.crews')}
- Spot Type
+ {t('mapUi.spotType')}
{Object.entries(SPOT_CATEGORIES).map(([key, cat]) => (
-
Sort By
+ {t('mapUi.sortBy')}
-
Trust Level
+
{t('mapUi.trustLevel')}
- Weather
+ {t('mapUi.weather')}
{
@@ -1332,18 +1336,18 @@ export default function MapPage() {
Stash) to avoid duplication. */}
{user && (
-
Quick Actions
+
{t('mapUi.quickActions')}
setMobileFiltersOpen(false)}>
- Buy/Sell Gear
+ {t('mapUi.buySellGear')}
setMobileFiltersOpen(false)}>
- Stash
+ {t('mapUi.stash')}
@@ -1357,7 +1361,7 @@ export default function MapPage() {
className="w-full h-12 font-bold text-base"
onClick={() => setMobileFiltersOpen(false)}
>
- Show Results
+ {t('mapUi.showResults')}
@@ -1391,22 +1395,22 @@ export default function MapPage() {
className="gap-1.5 font-bold text-xs px-2.5 h-8 flex-shrink-0 border-gray-700 hover:border-white/30 touch-manipulation active:scale-95"
>
- Layers
+ {t('mapUi.layers')}
{mobileLayerCount}
- Show on Map
+ {t('mapUi.showOnMap')}
{[
- { key: 'spots', label: 'Spots', dot: '#ff0040', active: showSpots, toggle: () => setShowSpots(v => !v) },
- { key: 'shops', label: 'Shops', dot: '#9333ea', active: showShops, toggle: () => setShowShops(v => !v) },
- { key: 'gear', label: 'Gear', dot: '#f97316', active: showGear, toggle: () => setShowGear(v => !v) },
- { key: 'tours', label: 'Tours', dot: '#06b6d4', active: showTours, toggle: () => setShowTours(v => !v) },
- { key: 'events', label: 'Events', dot: '#ccff00', active: showEvents, toggle: () => setShowEvents(v => !v) },
- { key: 'crews', label: 'Crews', dot: '#14b8a6', active: showCrews, toggle: () => setShowCrews(v => !v) },
+ { key: 'spots', label: t('mapUi.spots'), dot: '#ff0040', active: showSpots, toggle: () => setShowSpots(v => !v) },
+ { key: 'shops', label: t('mapUi.shops'), dot: '#9333ea', active: showShops, toggle: () => setShowShops(v => !v) },
+ { key: 'gear', label: t('mapUi.gear'), dot: '#f97316', active: showGear, toggle: () => setShowGear(v => !v) },
+ { key: 'tours', label: t('mapUi.tours'), dot: '#06b6d4', active: showTours, toggle: () => setShowTours(v => !v) },
+ { key: 'events', label: t('mapUi.events'), dot: '#ccff00', active: showEvents, toggle: () => setShowEvents(v => !v) },
+ { key: 'crews', label: t('mapUi.crews'), dot: '#14b8a6', active: showCrews, toggle: () => setShowCrews(v => !v) },
].map(({ key, label, dot, active, toggle }) => (
{showBtcMap
- ? `Showing ${btcMapPlaces?.length || 0} places where you can Spedn your sats nearby`
- : 'Show places where you can Spedn your sats (via BTC Map)'
+ ? t('mapUi.btcMapShowing', { count: btcMapPlaces?.length || 0 })
+ : t('mapUi.btcMapShow')
}
@@ -1473,7 +1477,7 @@ export default function MapPage() {
className="gap-1 font-bold text-xs px-2.5 h-8 flex-shrink-0 touch-manipulation active:scale-95 border-[#ff6600]/50 text-[#ff6600] hover:bg-[#ff6600]/10 hover:border-[#ff6600]"
>
- Stash
+ {t('mapUi.stash')}
@@ -1482,10 +1486,10 @@ export default function MapPage() {
- My Stash
+ {t('mapUi.myStash')}
- Encrypted
+ {t('mapUi.encrypted')}
@@ -1544,10 +1548,10 @@ export default function MapPage() {
- Local Chat
+ {t('mapUi.localChat')}
- Live chat for wherever the map is centered. Pick how wide the channel reaches.
+ {t('mapUi.localChatDesc')}
@@ -1581,7 +1585,7 @@ export default function MapPage() {
className="flex-shrink-0 gap-1 border-2 border-gray-700 hover:border-[#ccff00]/50 font-bold h-8 px-2 sm:px-3"
>
-
More
+
{t('mapUi.more')}
@@ -1590,10 +1594,10 @@ export default function MapPage() {
{t('shops.addShop')}
-
Buy/Sell Gear
+
{t('mapUi.buySellGear')}
-
Offer Tour
+
{t('mapUi.offerTour')}
@@ -1639,7 +1643,7 @@ export default function MapPage() {
-
Loading map...
+
{t('mapUi.loadingMap')}
}>
@@ -1687,7 +1691,7 @@ export default function MapPage() {
- Loading spots
+ {t('mapUi.loadingSpots')}
@@ -1706,7 +1710,7 @@ export default function MapPage() {
- Offline · showing saved spots
+ {t('mapUi.offlineSavedSpots')}
@@ -1723,7 +1727,7 @@ export default function MapPage() {
type="button"
onClick={() => setOfflineBannerDismissed(true)}
className="rounded-full p-1 text-amber-200/60 transition-colors hover:bg-white/10 hover:text-white"
- aria-label="Dismiss"
+ aria-label={t('mapUi.dismiss')}
>
@@ -1737,10 +1741,10 @@ export default function MapPage() {
{!isLoading && (showSpots || showShops) && filteredSpots.length === 0 && allLocations.length > 0 && (
-
No spots match
-
Try a different search or clear your filters.
+
{t('mapUi.noSpotsMatchTitle')}
+
{t('mapUi.noSpotsMatchDesc')}
- Clear filters
+ {t('common.clearFilters')}
@@ -1754,13 +1758,13 @@ export default function MapPage() {
- Can't reach the relays
+ {t('mapUi.cantReachRelays')}
{online
- ? "You're online, but the community relay isn't answering right now."
- : "You're offline."}{' '}
- Spots you've already seen are saved on this device and will show here.
+ ? t('mapUi.relayOnlineNoAnswer')
+ : t('mapUi.youreOffline')}{' '}
+ {t('mapUi.savedOnDevice')}
- Try again
+ {t('common.tryAgain')}
@@ -1786,11 +1790,10 @@ export default function MapPage() {
🛹
- No spots mapped yet
+ {t('mapUi.noSpotsMappedYet')}
- Be the first to map your park — it takes a minute, it's yours, and the next
- skater who rolls through will thank you.
+ {t('mapUi.noSpotsMappedDesc')}
@@ -1799,12 +1802,12 @@ export default function MapPage() {
className="w-full gap-2 bg-[#ccff00] font-black uppercase text-black hover:bg-[#ccff00]/90"
>
- Add the first spot
+ {t('mapUi.addFirstSpot')}
- {nearMeLoading ? 'Locating…' : 'Find spots near me'}
+ {nearMeLoading ? t('mapUi.locating') : t('mapUi.findSpotsNearMe')}
@@ -1837,18 +1840,18 @@ export default function MapPage() {
-
Location Blocked
+
{t('mapUi.locationBlocked')}
- Your browser has blocked location access for this site. Here's how to re-enable it:
+ {t('mapUi.locationBlockedDesc')}
{[
- { platform: '📱 Android Chrome', steps: 'Tap the lock icon in the address bar → Site settings → Location → Allow' },
- { platform: '🍎 iOS Safari', steps: 'Settings app → Safari → Location → Allow (or tap the "AA" button in Safari\'s address bar)' },
- { platform: '💻 Desktop Chrome', steps: 'Click the lock icon → Site settings → Location → Allow' },
- { platform: '🦊 Firefox', steps: 'Click the lock icon → Connection secure → More info → Permissions → Access your location → Allow' },
+ { platform: `📱 ${t('mapUi.locationPlatformAndroid')}`, steps: t('mapUi.locationStepsAndroid') },
+ { platform: `🍎 ${t('mapUi.locationPlatformIos')}`, steps: t('mapUi.locationStepsIos') },
+ { platform: `💻 ${t('mapUi.locationPlatformDesktop')}`, steps: t('mapUi.locationStepsDesktop') },
+ { platform: `🦊 ${t('mapUi.locationPlatformFirefox')}`, steps: t('mapUi.locationStepsFirefox') },
].map(({ platform, steps }) => (
{platform}
@@ -1863,7 +1866,7 @@ export default function MapPage() {
}}
className="w-full py-3 rounded-xl bg-white/8 hover:bg-white/12 border border-white/10 text-sm font-bold text-white transition-all active:scale-[0.98]"
>
- Got it
+ {t('mapUi.gotIt')}
@@ -1898,7 +1901,7 @@ export default function MapPage() {
? 'bg-red-900/80 border-red-500/30'
: 'bg-black/70 border-white/15 hover:border-white/30'
)}
- aria-label="Find spots near me"
+ aria-label={t('mapUi.findSpotsNearMe')}
>
{nearMeLoading ? (
@@ -1919,7 +1922,7 @@ export default function MapPage() {
? 'bg-[#ff0040]/20 border-[#ff0040]/60'
: 'bg-black/70 border-white/15 hover:border-white/30'
)}
- aria-label="Map layers"
+ aria-label={t('mapUi.mapLayers')}
>
{mobileLayerCount < totalPrimaryLayers && (
@@ -1934,15 +1937,15 @@ export default function MapPage() {
align="end"
className="w-52 p-2 bg-black/95 border-white/15 backdrop-blur-xl"
>
- Show on Map
+ {t('mapUi.showOnMap')}
{[
- { key: 'spots', label: 'Spots', dot: '#ff0040', active: showSpots, toggle: () => setShowSpots(v => !v) },
- { key: 'shops', label: 'Shops', dot: '#9333ea', active: showShops, toggle: () => setShowShops(v => !v) },
- { key: 'gear', label: 'Gear', dot: '#f97316', active: showGear, toggle: () => setShowGear(v => !v) },
- { key: 'tours', label: 'Tours', dot: '#06b6d4', active: showTours, toggle: () => setShowTours(v => !v) },
- { key: 'events', label: 'Events', dot: '#ccff00', active: showEvents, toggle: () => setShowEvents(v => !v) },
- { key: 'crews', label: 'Crews', dot: '#14b8a6', active: showCrews, toggle: () => setShowCrews(v => !v) },
+ { key: 'spots', label: t('mapUi.spots'), dot: '#ff0040', active: showSpots, toggle: () => setShowSpots(v => !v) },
+ { key: 'shops', label: t('mapUi.shops'), dot: '#9333ea', active: showShops, toggle: () => setShowShops(v => !v) },
+ { key: 'gear', label: t('mapUi.gear'), dot: '#f97316', active: showGear, toggle: () => setShowGear(v => !v) },
+ { key: 'tours', label: t('mapUi.tours'), dot: '#06b6d4', active: showTours, toggle: () => setShowTours(v => !v) },
+ { key: 'events', label: t('mapUi.events'), dot: '#ccff00', active: showEvents, toggle: () => setShowEvents(v => !v) },
+ { key: 'crews', label: t('mapUi.crews'), dot: '#14b8a6', active: showCrews, toggle: () => setShowCrews(v => !v) },
].map(({ key, label, dot, active, toggle }) => (
{[
- { label: 'Add Spot', to: '/create-spot', bg: 'bg-[#ff0040]', icon: },
- { label: 'Add Shop', to: '/create-shop', bg: 'bg-[#9333ea]', icon: },
- { label: 'Add Event', to: '/create-event', bg: 'bg-[#ccff00]', icon: },
- { label: 'Sell Gear', to: '/create-listing', bg: 'bg-[#f97316]', icon: },
- { label: 'Offer Tour', to: '/create-tour', bg: 'bg-[#06b6d4]', icon: },
+ { label: t('mapUi.addSpot'), to: '/create-spot', bg: 'bg-[#ff0040]', icon: },
+ { label: t('mapUi.addShop'), to: '/create-shop', bg: 'bg-[#9333ea]', icon: },
+ { label: t('mapUi.addEvent'), to: '/create-event', bg: 'bg-[#ccff00]', icon: },
+ { label: t('mapUi.sellGear'), to: '/create-listing', bg: 'bg-[#f97316]', icon: },
+ { label: t('mapUi.offerTour'), to: '/create-tour', bg: 'bg-[#06b6d4]', icon: },
].map((a, i) => (
@@ -2097,7 +2100,7 @@ export default function MapPage() {
hideClose
className="h-auto max-h-[55vh] rounded-t-2xl px-0 pb-0 bg-black/95 border-t border-white/10"
>
-
{selectedSpot?.title ?? 'Spot details'}
+
{selectedSpot?.title ?? t('mapUi.spotDetails')}
{selectedSpot && (
('browse');
@@ -32,7 +34,7 @@ export default function MarketplaceHubPage() {
- {TABS.map(({ id, label, icon: Icon }) => {
+ {TABS.map(({ id, labelKey, icon: Icon }) => {
const active = tab === id;
return (
- {label}
+ {t(labelKey)}
);
})}
@@ -61,7 +63,7 @@ export default function MarketplaceHubPage() {
) : (
-
Log in to manage your listings.
+
{t('marketUi.hub.loginListings')}
)}
@@ -74,7 +76,7 @@ export default function MarketplaceHubPage() {
) : (
-
Log in to see your reputation and reviews.
+
{t('marketUi.hub.loginReputation')}
)}
diff --git a/src/pages/MarketplacePage.tsx b/src/pages/MarketplacePage.tsx
index 5c19891..d6739a4 100644
--- a/src/pages/MarketplacePage.tsx
+++ b/src/pages/MarketplacePage.tsx
@@ -3,8 +3,10 @@ import { Card, CardContent } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { ShoppingBag, ExternalLink, Package, DollarSign, Shield, Zap, Store } from 'lucide-react';
+import { useTranslation } from '@/contexts/language';
export default function MarketplacePage() {
+ const { t } = useTranslation();
useSeoMeta({
title: 'Marketplace - SkateSpots',
description: 'Buy and sell skateboarding gear on the decentralized Plebeian Market.',
@@ -24,11 +26,10 @@ export default function MarketplacePage() {
fontFamily: 'Impact, "Arial Black", sans-serif',
letterSpacing: '0.05em'
}}>
- Shops & Marketplace
+ {t('marketUi.marketplace.heroTitle')}
- Our marketplace is powered by Plebeian Market - a decentralized marketplace on Nostr.
- Skate shops can list their products, and you can buy/sell used gear directly with Bitcoin.
+ {t('marketUi.marketplace.heroBefore')} Plebeian Market {t('marketUi.marketplace.heroAfter')}
@@ -40,10 +41,9 @@ export default function MarketplacePage() {
-
What is Plebeian Market?
+
{t('marketUi.marketplace.whatIsTitle')}
- Plebeian Market is a decentralized marketplace built on Nostr. Buy and sell skateboarding gear
- directly with other skaters using Bitcoin. No middlemen, no fees, total freedom.
+ {t('marketUi.marketplace.whatIsBody')}
@@ -51,18 +51,18 @@ export default function MarketplacePage() {
-
No Fees
-
No platform taking 30%. Direct peer-to-peer sales.
+
{t('marketUi.marketplace.noFeesTitle')}
+
{t('marketUi.marketplace.noFeesBody')}
-
Bitcoin Payments
-
Pay with Bitcoin. Fast, secure, global.
+
{t('marketUi.marketplace.bitcoinTitle')}
+
{t('marketUi.marketplace.bitcoinBody')}
-
Decentralized
-
Built on Nostr. No company controls it.
+
{t('marketUi.marketplace.decentralizedTitle')}
+
{t('marketUi.marketplace.decentralizedBody')}
@@ -70,7 +70,7 @@ export default function MarketplacePage() {
- Visit Plebeian Market
+ {t('marketUi.marketplace.visit')}
@@ -79,27 +79,51 @@ export default function MarketplacePage() {
{/* What You Can Buy/Sell */}
-
What You Can Buy & Sell
+
{t('marketUi.marketplace.buySellTitle')}
{[
{
- title: 'Complete Decks',
- items: ['Pre-built completes', 'Custom setups', 'Beginner boards'],
+ title: t('marketUi.marketplace.catCompletes'),
+ items: [
+ t('marketUi.marketplace.preBuilt'),
+ t('marketUi.marketplace.customSetups'),
+ t('marketUi.marketplace.beginnerBoards'),
+ ],
color: '#ff0040'
},
{
- title: 'Deck Parts',
- items: ['Decks', 'Trucks', 'Wheels', 'Bearings', 'Grip tape', 'Hardware'],
+ title: t('marketUi.marketplace.catParts'),
+ items: [
+ t('marketUi.marketplace.decks'),
+ t('marketUi.marketplace.trucks'),
+ t('marketUi.marketplace.wheels'),
+ t('marketUi.marketplace.bearings'),
+ t('marketUi.marketplace.gripTape'),
+ t('marketUi.marketplace.hardware'),
+ ],
color: '#ff6600'
},
{
- title: 'Apparel & Protection',
- items: ['Skate shoes', 'Clothing', 'Helmets', 'Pads', 'Backpacks'],
+ title: t('marketUi.marketplace.catApparel'),
+ items: [
+ t('marketUi.marketplace.skateShoes'),
+ t('marketUi.marketplace.clothing'),
+ t('marketUi.marketplace.helmets'),
+ t('marketUi.marketplace.pads'),
+ t('marketUi.marketplace.backpacks'),
+ ],
color: '#ccff00'
},
{
- title: 'Accessories',
- items: ['Tools', 'Wax', 'Rails', 'Stickers', 'Videos', 'Magazines'],
+ title: t('marketUi.marketplace.catAccessories'),
+ items: [
+ t('marketUi.marketplace.tools'),
+ t('marketUi.marketplace.wax'),
+ t('marketUi.marketplace.rails'),
+ t('marketUi.marketplace.stickers'),
+ t('marketUi.marketplace.videos'),
+ t('marketUi.marketplace.magazines'),
+ ],
color: '#ff0040'
},
].map((category, i) => (
@@ -125,29 +149,29 @@ export default function MarketplacePage() {
{/* How It Works */}
- How It Works
+ {t('marketUi.marketplace.howItWorks')}
{[
{
num: 1,
- title: 'Browse Products',
- desc: 'Visit Plebeian Market and browse skateboarding gear from shops and individuals worldwide.',
+ title: t('marketUi.marketplace.step1Title'),
+ desc: t('marketUi.marketplace.step1Body'),
},
{
num: 2,
- title: 'Contact Sellers',
- desc: 'Message sellers directly via Nostr. Negotiate prices, ask questions, arrange shipping.',
+ title: t('marketUi.marketplace.step2Title'),
+ desc: t('marketUi.marketplace.step2Body'),
},
{
num: 3,
- title: 'Pay with Bitcoin',
- desc: 'Complete your purchase using Bitcoin Lightning for instant, low-fee payments.',
+ title: t('marketUi.marketplace.step3Title'),
+ desc: t('marketUi.marketplace.step3Body'),
},
{
num: 4,
- title: 'Leave Feedback',
- desc: 'Rate your experience and build seller reputation on Nostr. Your reviews help the community.',
+ title: t('marketUi.marketplace.step4Title'),
+ desc: t('marketUi.marketplace.step4Body'),
},
].map((step) => (
@@ -166,10 +190,9 @@ export default function MarketplacePage() {
-
Need Bitcoin?
+
{t('marketUi.marketplace.needBitcoinTitle')}
- You'll need a Lightning wallet with some sats to make purchases.
- Check out the onboarding guide for wallet recommendations.
+ {t('marketUi.marketplace.needBitcoinBefore')} {t('marketUi.marketplace.onboardingGuide')} {t('marketUi.marketplace.needBitcoinAfter')}
@@ -181,22 +204,21 @@ export default function MarketplacePage() {
- Own a Skate Shop?
+ {t('marketUi.marketplace.ownShopTitle')}
- List your shop on SkateSpots and sell your products on Plebeian Market.
- Reach customers worldwide with zero platform fees.
+ {t('marketUi.marketplace.ownShopBody')}
diff --git a/src/pages/PyramidPage.tsx b/src/pages/PyramidPage.tsx
index b5d9423..58b642e 100644
--- a/src/pages/PyramidPage.tsx
+++ b/src/pages/PyramidPage.tsx
@@ -9,10 +9,12 @@ import { useAuthor } from '@/hooks/useAuthor';
import { useCurrentUser } from '@/hooks/useCurrentUser';
import { useTrustScore } from '@/hooks/useTrustScore';
import { genUserName } from '@/lib/genUserName';
+import { useTranslation } from '@/contexts/language';
import { Trophy, Medal } from 'lucide-react';
import { cn } from '@/lib/utils';
function ContributorRow({ pubkey, spots, events, rank }: { pubkey: string; spots: number; events: number; rank: number }) {
+ const { t } = useTranslation();
const author = useAuthor(pubkey);
const { data: trustScore } = useTrustScore(pubkey);
const metadata = author.data?.metadata;
@@ -51,21 +53,21 @@ function ContributorRow({ pubkey, spots, events, rank }: { pubkey: string; spots
{spots}
-
Spots
+
{t('labUi.pyramid.spots')}
{events}
-
Events
+
{t('labUi.pyramid.events')}
{total}
-
Total
+
{t('labUi.pyramid.total')}
{trustScore ?? '—'}
-
Trust
+
{t('labUi.pyramid.trust')}
@@ -73,6 +75,7 @@ function ContributorRow({ pubkey, spots, events, rank }: { pubkey: string; spots
}
export default function PyramidPage() {
+ const { t } = useTranslation();
useSeoMeta({
title: 'Pyramid Rankings - SkateSpots',
description: 'Top contributors and community leaders.',
@@ -121,9 +124,9 @@ export default function PyramidPage() {
- PYRAMID RANKINGS
+ {t('labUi.pyramid.pyramidWord')} {t('labUi.pyramid.rankingsWord')}
-
Top contributors in your city
+
{t('labUi.pyramid.subtitle')}
{/* Your Stats */}
@@ -134,22 +137,22 @@ export default function PyramidPage() {
#{userRank}
-
Your Rank
-
Keep contributing to climb higher!
+
{t('labUi.pyramid.yourRank')}
+
{t('labUi.pyramid.keepContributing')}
{userStats.spots}
-
Spots
+
{t('labUi.pyramid.spots')}
{userStats.events}
-
Events
+
{t('labUi.pyramid.events')}
{userStats.total}
-
Total
+
{t('labUi.pyramid.total')}
{userTrustScore ?? '—'}
-
Trust
+
{t('labUi.pyramid.trust')}
@@ -174,7 +177,7 @@ export default function PyramidPage() {
-
Top Contributors
+ {t('labUi.pyramid.topContributors')}
{rankings.length > 0 ? (
@@ -192,7 +195,7 @@ export default function PyramidPage() {
) : (
-
No contributors yet. Be the first to add a spot or create an event!
+
{t('labUi.pyramid.noContributors')}
)}
diff --git a/src/pages/RiderCardPage.tsx b/src/pages/RiderCardPage.tsx
index 9d1e4b5..b8d18bf 100644
--- a/src/pages/RiderCardPage.tsx
+++ b/src/pages/RiderCardPage.tsx
@@ -36,6 +36,7 @@ import { useToast } from '@/hooks/useToast';
import { useNostr } from '@/hooks/useNostr';
import { useQuery } from '@tanstack/react-query';
import { genUserName } from '@/lib/genUserName';
+import { useTranslation } from '@/contexts/language';
import { cn } from '@/lib/utils';
import type { NostrEvent } from '@nostrify/nostrify';
import { Printer, Loader2, Share2, Download, Check, Copy, Award, Zap, Info, Palette, Shield, ChevronUp, ChevronDown, User, Building2 } from 'lucide-react';
@@ -627,6 +628,7 @@ function useWitnessName(pubkey: string) {
// Layout is STRICTLY pixel-fixed to match the 340×214px card container.
// No flex-1 / flex-grow — every row has a fixed pixel height.
function RiderFront({ d }: { d: CardData }) {
+ const { t } = useTranslation();
const witnessedBadges = d.receivedBadges.slice(0, 2);
return (
@@ -639,14 +641,14 @@ function RiderFront({ d }: { d: CardData }) {
🛹 SkateSpots
- Skater License
+ {t('labUi.rider.previewLicense')}
npub
{shortNpub(d.pubkey)}
-
Since {d.memberYear}
+
{t('labUi.rider.previewSince', { year: d.memberYear })}
@@ -666,7 +668,7 @@ function RiderFront({ d }: { d: CardData }) {
{d.showSpots && (
- Spots
+ {t('labUi.pyramid.spots')}
{d.spotsAdded}
)}
@@ -678,7 +680,7 @@ function RiderFront({ d }: { d: CardData }) {
)}
{d.showEvents && (
- Evts
+ {t('labUi.rider.previewEvts')}
{d.eventsCreated}
)}
@@ -736,7 +738,7 @@ function RiderFront({ d }: { d: CardData }) {
))}
- Scan to verify
+ {t('labUi.rider.previewScan')}
@@ -758,6 +760,7 @@ function RiderFront({ d }: { d: CardData }) {
// ─── Card visual: Park Access front ──────────────────────────────────────────
function ParkFront({ d }: { d: CardData }) {
+ const { t } = useTranslation();
return (
@@ -766,9 +769,9 @@ function ParkFront({ d }: { d: CardData }) {
- 🏟️ {d.issuerName || 'Skatepark'}
+ 🏟️ {d.issuerName || t('labUi.rider.previewSkatepark')}
-
Access Pass
+
{t('labUi.rider.previewAccessPass')}
{d.accessLevel || 'Full Access'}
@@ -791,12 +794,12 @@ function ParkFront({ d }: { d: CardData }) {
{/* Identity */}
-
{d.recipientName || 'Member Name'}
+
{d.recipientName || t('labUi.rider.previewMemberName')}
{shortNpub(d.pubkey)}
-
Issued by park owner · Nostr-signed
+
{t('labUi.rider.previewIssuedBy')}
{d.validThru && (
- Valid thru
+ {t('labUi.rider.previewValidThru')}
{d.validThru}
)}
@@ -811,7 +814,7 @@ function ParkFront({ d }: { d: CardData }) {
))}
-
Scan to verify
+
{t('labUi.rider.previewScan')}
@@ -825,7 +828,7 @@ function ParkFront({ d }: { d: CardData }) {
- Nostr-verified
+ {t('labUi.rider.previewNostrVerified')}
@@ -854,6 +857,7 @@ function RiderCardFront({ d, printRef }: { d: CardData; printRef?: React.RefObje
// ─── Received badges panel ────────────────────────────────────────────────────
function ReceivedBadgesPanel({ pubkey }: { pubkey: string }) {
+ const { t } = useTranslation();
const { data: badgeEvents = [] } = useReceivedBadges(pubkey);
const [expanded, setExpanded] = useState(false);
@@ -861,7 +865,7 @@ function ReceivedBadgesPanel({ pubkey }: { pubkey: string }) {
return (
🏅
-
No badges yet. Skate in front of someone and ask them to witness your tricks!
+
{t('labUi.rider.noBadges')}
);
}
@@ -879,7 +883,7 @@ function ReceivedBadgesPanel({ pubkey }: { pubkey: string }) {
className="w-full text-xs text-gray-500 hover:text-gray-300 flex items-center justify-center gap-1 py-1"
>
{expanded ? : }
- {expanded ? 'Show less' : `Show ${badgeEvents.length - 3} more`}
+ {expanded ? t('common.showLess') : t('labUi.rider.showMore', { count: badgeEvents.length - 3 })}
)}
@@ -887,6 +891,7 @@ function ReceivedBadgesPanel({ pubkey }: { pubkey: string }) {
}
function BadgeEventRow({ event }: { event: NostrEvent }) {
+ const { t } = useTranslation();
const skills = event.tags.filter(([k]) => k === 'skill').map(([, v]) => v);
const spot = event.tags.find(([k]) => k === 'spot')?.[1];
const witnessName = useWitnessName(event.pubkey);
@@ -913,8 +918,8 @@ function BadgeEventRow({ event }: { event: NostrEvent }) {
})}
- Witnessed by {witnessName}
- {spot && <> at {spot} >}
+ {t('labUi.rider.witnessedBy')} {witnessName}
+ {spot && <> {t('labUi.rider.at')} {spot} >}
@@ -926,6 +931,7 @@ function BadgeEventRow({ event }: { event: NostrEvent }) {
// ─── Page ─────────────────────────────────────────────────────────────────────
export default function RiderCardPage() {
+ const { t } = useTranslation();
useSeoMeta({
title: 'Skater Card — SkateSpots',
description: 'Create your printable SkateSpots Skater License or Park Access Card.',
@@ -1038,23 +1044,23 @@ export default function RiderCardPage() {
const handlePrint = useCallback(async () => {
const dataUrl = await renderCardToImage();
- if (!dataUrl) { toast({ title: 'Could not render card', variant: 'destructive' }); return; }
+ if (!dataUrl) { toast({ title: t('labUi.rider.couldNotRender'), variant: 'destructive' }); return; }
const w = Math.round(85.6 * 2.835);
const h = Math.round(54 * 2.835);
const win = window.open('', '_blank', `width=${w * 2},height=${h * 2}`);
- if (!win) { toast({ title: 'Popup blocked', description: 'Allow popups for this site.', variant: 'destructive' }); return; }
+ if (!win) { toast({ title: t('labUi.rider.popupBlocked'), description: t('labUi.rider.popupBlockedDesc'), variant: 'destructive' }); return; }
win.document.write(`
SkateSpots Rider Card window.onload=function(){window.focus();window.print()} `);
win.document.close();
- }, [renderCardToImage, toast]);
+ }, [renderCardToImage, toast, t]);
const handleDownload = useCallback(async () => {
const dataUrl = await renderCardToImage();
- if (!dataUrl) { toast({ title: 'Could not render card', variant: 'destructive' }); return; }
+ if (!dataUrl) { toast({ title: t('labUi.rider.couldNotRender'), variant: 'destructive' }); return; }
const a = document.createElement('a');
a.href = dataUrl;
a.download = `skatespot-card-${cardData.npub.slice(0, 16)}.png`;
a.click();
- }, [renderCardToImage, cardData.npub, toast]);
+ }, [renderCardToImage, cardData.npub, toast, t]);
const handleCopyNpub = useCallback(() => {
navigator.clipboard.writeText(npub);
@@ -1064,7 +1070,7 @@ export default function RiderCardPage() {
const handleShare = useCallback(async () => {
const dataUrl = await renderCardToImage();
- if (!dataUrl) { toast({ title: 'Could not render card', variant: 'destructive' }); return; }
+ if (!dataUrl) { toast({ title: t('labUi.rider.couldNotRender'), variant: 'destructive' }); return; }
const res = await fetch(dataUrl);
const blob = await res.blob();
const file = new File([blob], `skatespot-card-${cardData.name.replace(/\s+/g, '-')}.png`, { type: 'image/png' });
@@ -1075,11 +1081,11 @@ export default function RiderCardPage() {
} catch { /* fall through */ }
}
await navigator.clipboard.writeText(`https://skate-spots.org/${cardData.npub}`);
- toast({ title: '🔗 Link copied!' });
- }, [renderCardToImage, cardData.name, cardData.npub, toast]);
+ toast({ title: t('labUi.rider.linkCopied') });
+ }, [renderCardToImage, cardData.name, cardData.npub, toast, t]);
const handlePublishParkCard = useCallback(() => {
- if (!user || !recipientPubkey) { toast({ title: "Enter the rider's npub", variant: 'destructive' }); return; }
+ if (!user || !recipientPubkey) { toast({ title: t('labUi.rider.enterRiderNpub'), variant: 'destructive' }); return; }
const tags: string[][] = [
['d', `park-access-${recipientPubkey.slice(0, 16)}-${Date.now()}`],
['p', recipientPubkey], ['t', 'skatepark-access'], ['alt', 'Skatepark Access Card'],
@@ -1087,10 +1093,10 @@ export default function RiderCardPage() {
];
if (validThru) tags.push(['valid-thru', validThru]);
publishEvent({ kind: PARK_ACCESS_CARD_KIND, content: '', tags }, {
- onSuccess: () => toast({ title: '✅ Park Access Card published!' }),
- onError: (e) => toast({ title: 'Publish failed', description: String(e), variant: 'destructive' }),
+ onSuccess: () => toast({ title: t('labUi.rider.parkPublished') }),
+ onError: (e) => toast({ title: t('labUi.rider.publishFailed'), description: String(e), variant: 'destructive' }),
});
- }, [user, recipientPubkey, issuerName, accessLevel, validThru, theme, publishEvent, toast]);
+ }, [user, recipientPubkey, issuerName, accessLevel, validThru, theme, publishEvent, toast, t]);
const toggleSkill = (id: string) =>
setSkills(prev => prev.includes(id) ? prev.filter(s => s !== id) : [...prev, id]);
@@ -1110,10 +1116,10 @@ export default function RiderCardPage() {
CARD
- Your verifiable skater identity. Nostr-signed, print-ready, impossible to fake.
+ {t('labUi.rider.heroDesc')}
- Earn witnessed skill badges from other skaters. Get park access passes. Share your card anywhere.
+ {t('labUi.rider.heroDesc2')}
@@ -1131,15 +1137,15 @@ export default function RiderCardPage() {
RIDER
CARD
- Nostr-signed · Print-ready · QR-verified
+ {t('labUi.rider.headerTagline')}
{/* ── Top tabs: My Card / My Badges / Award Badge ── */}
{([
- { id: 'card', label: '🪪 My Card' },
- { id: 'badges', label: `🏅 Badges${receivedBadges.length > 0 ? ` (${receivedBadges.length})` : ''}` },
- { id: 'award', label: '⭐ Award' },
+ { id: 'card', label: `🪪 ${t('labUi.rider.tabCard')}` },
+ { id: 'badges', label: `🏅 ${t('labUi.rider.tabBadges')}${receivedBadges.length > 0 ? ` (${receivedBadges.length})` : ''}` },
+ { id: 'award', label: `⭐ ${t('labUi.rider.tabAward')}` },
] as const).map(tab => (
{rendering ? : }
- {rendering ? 'Rendering…' : 'Share Card'}
+ {rendering ? t('labUi.rider.rendering') : t('labUi.rider.shareCard')}
{/* Secondary actions row */}
@@ -1196,7 +1202,7 @@ export default function RiderCardPage() {
className="gap-1.5 border-white/10 hover:border-white/30 text-xs font-bold uppercase h-10"
>
- Save PNG
+ {t('labUi.rider.savePng')}
- Print
+ {t('labUi.rider.print')}
{copied ? : }
- {copied ? 'Copied!' : 'Copy ID'}
+ {copied ? t('common.copied') : t('labUi.rider.copyId')}
{/* ── Card type toggle ── */}