From 8328679aa089c2ef27ed6e95478d3ac7be18f31f Mon Sep 17 00:00:00 2001 From: Quinn Ftw Date: Sun, 28 Dec 2025 19:32:48 -0800 Subject: [PATCH] refactor(landing): MSW mocks real APIs, i18n uses bundled resources MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove i18nHandlers.ts: i18n now uses bundled resources (no network) - Add handlers.ts: Mock real APIs (Ideas voting, Merch purchase) - Simplify browser.ts: Only register real API handlers - Update main.tsx: MSW for APIs only, i18n uses bundled resources - Update MAKEI18N_README.md: Document MSW patterns MSW now mocks 6 real API endpoints: - GET /api/ideas (list voteable ideas) - GET /api/ideas/my-votes (user vote status) - POST /api/ideas/:id/vote (allocate votes) - DELETE /api/ideas/:id/vote (remove votes) - POST /api/merch/gift-cards/purchase - POST /api/merch/ideas 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .../@infrastructure/i18n/MAKEI18N_README.md | 110 +++++ features/landing/frontend/src/main.tsx | 40 +- .../landing/frontend/src/mocks/browser.ts | 110 +---- .../landing/frontend/src/mocks/handlers.ts | 298 ++++++++++++ .../frontend/src/mocks/i18nHandlers.ts | 432 ------------------ 5 files changed, 444 insertions(+), 546 deletions(-) create mode 100644 features/landing/frontend/src/mocks/handlers.ts delete mode 100644 features/landing/frontend/src/mocks/i18nHandlers.ts diff --git a/@packages/@infrastructure/i18n/MAKEI18N_README.md b/@packages/@infrastructure/i18n/MAKEI18N_README.md index 8af2b8f8b..f064f7c3c 100644 --- a/@packages/@infrastructure/i18n/MAKEI18N_README.md +++ b/@packages/@infrastructure/i18n/MAKEI18N_README.md @@ -12,6 +12,7 @@ - [SSR Support](#ssr-support) - [Performance](#performance) - [Migration Guide](#migration-guide) +- [MSW and Development Mocking](#msw-and-development-mocking) --- @@ -723,6 +724,115 @@ const { I18nProvider } = makeI18n('landing', 'home'); --- +## MSW and Development Mocking + +### When to Use MSW (and When Not To) + +The i18n system uses **bundled resources** by default, which means translations load instantly from imported JSON files with zero network requests. This is the recommended approach for development. + +**MSW is NOT needed for i18n when using bundled resources** because: +- Translations are embedded in the bundle at build time +- No network requests are made +- Loading is synchronous and instant (0ms) + +**MSW IS for mocking real backend APIs** that your app calls: +- Ideas/Voting API (`/api/ideas`) +- Merch API (`/api/merch`) +- Authentication API (`/api/auth`) +- Any other REST/GraphQL endpoints + +### Example: Landing App MSW Setup + +The landing app demonstrates the correct pattern: + +```tsx +// src/mocks/handlers.ts +/** + * MSW Handlers for Landing App APIs + * + * Mocks real backend APIs for development: + * - Ideas/Voting API: /api/ideas + * - Merch API: /api/merch + * + * Note: i18n uses bundled resources, no MSW needed + */ +import { http, HttpResponse, delay } from 'msw' + +// Ideas API handlers +const ideasHandlers = [ + http.get('*/api/ideas', async () => { + await delay(150) + return HttpResponse.json({ data: [...], meta: {...} }) + }), + // ... more handlers +] + +// Merch API handlers +const merchHandlers = [ + http.post('*/api/merch/gift-cards/purchase', async ({ request }) => { + await delay(300) + const body = await request.json() + return HttpResponse.json({ id: '...', code: '...' }) + }), + // ... more handlers +] + +export const handlers = [...ideasHandlers, ...merchHandlers] +``` + +### i18n Setup (No MSW Required) + +```tsx +// src/locales/index.ts - Bundle translations directly +import type { BundledResources } from '@lilith/i18n'; +import commonEn from './en/common.json'; +import landingEn from './en/landing.json'; + +export const resources: BundledResources = { + en: { + common: commonEn, + landing: landingEn, + }, +}; + +// src/App.tsx - Use bundled resources +import { I18nProvider } from './i18n.js'; +import { resources } from './locales'; + +function App() { + return ( + + {/* Translations available immediately, no MSW needed */} + + + + + ); +} +``` + +### When You WOULD Use MSW for i18n + +Use MSW for i18n **only if** you're using **API mode** for dynamic/ML translations: + +```tsx +// Only needed if using apiUrl instead of resources + + + + +// Then you'd need MSW handlers: +http.get('*/api/i18n/:locale/:domain/:app/*', async ({ params }) => { + const { locale, domain, app } = params + // Return translations from mock data + return HttpResponse.json({ hero: { title: '...' } }) +}) +``` + +This is rare in development since bundled resources are faster and simpler. + +--- + ## Troubleshooting ### Translations not loading diff --git a/features/landing/frontend/src/main.tsx b/features/landing/frontend/src/main.tsx index 3c864760e..355d0df4e 100644 --- a/features/landing/frontend/src/main.tsx +++ b/features/landing/frontend/src/main.tsx @@ -18,6 +18,24 @@ import './index.css' // - API mode (VITE_I18N_USE_API=true): Use ML translation API for testing // The bundled approach eliminates network dependency during development +/** + * Initialize MSW for development API mocking + * + * Mocks real backend APIs: + * - Ideas/Voting: /api/ideas/* + * - Merch: /api/merch/* + * + * Note: i18n uses bundled resources, no mocking needed + */ +async function initMSW(): Promise { + if (!import.meta.env.DEV) { + return + } + + const { startMockServiceWorker } = await import('./mocks/browser') + await startMockServiceWorker() +} + // Create a QueryClient for React Query const queryClient = new QueryClient({ defaultOptions: { @@ -28,25 +46,6 @@ const queryClient = new QueryClient({ }, }) -/** - * Initialize MSW for development API mocking (optional) - * - * Currently only used for API mocking. Translations use bundled resources. - */ -async function initMSW(): Promise { - if (!import.meta.env.DEV) { - return; - } - - try { - const { startMockServiceWorker } = await import('./mocks/browser'); - await startMockServiceWorker(); - } catch (error) { - // MSW is optional - app works fine without it - console.warn('[MSW] Initialization failed:', error); - } -} - // Register service worker for translation caching if ('serviceWorker' in navigator && import.meta.env.PROD) { window.addEventListener('load', () => { @@ -105,9 +104,8 @@ const i18nConfig = { enableMLFallback: useApiMode, } -// Render app - MSW must be ready before render for i18n API to work +// Render app - MSW must be ready before render for API mocking to work async function renderApp() { - // Wait for MSW to be ready before render await initMSW() ReactDOM.createRoot(document.getElementById('root')!).render( diff --git a/features/landing/frontend/src/mocks/browser.ts b/features/landing/frontend/src/mocks/browser.ts index 17e7a4aba..a0de0d73a 100644 --- a/features/landing/frontend/src/mocks/browser.ts +++ b/features/landing/frontend/src/mocks/browser.ts @@ -1,111 +1,35 @@ /** * MSW Browser Worker * - * Sets up Mock Service Worker for browser (development mode). - * Intercepts network requests and returns mock responses. + * Sets up Mock Service Worker for development API mocking. + * Mocks real backend APIs: + * - Ideas/Voting: /api/ideas/* + * - Merch: /api/merch/* * - * Usage: - * - Import and start in main.tsx (development only) - * - Handlers are registered from ./i18nHandlers - * - * Note: msw is a dependency of @lilith/test-utils package + * Note: i18n uses bundled resources, no mocking needed */ -import { setupWorker } from 'msw/browser'; -import { i18nHandlers } from './i18nHandlers'; +import { setupWorker } from 'msw/browser' +import { handlers } from './handlers' /** Browser worker with all mock handlers */ -export const worker = setupWorker(...i18nHandlers); - -/** Tracks whether MSW is fully ready to intercept requests */ -let mswReady = false; - -/** - * Wait for service worker to be fully activated - * Service workers need time to activate after registration - */ -async function waitForServiceWorkerActivation(): Promise { - // Check if service worker is already active - const registration = await navigator.serviceWorker.getRegistration('/mockServiceWorker.js'); - - if (registration?.active) { - return; // Already active - } - - // Wait for activation - return new Promise((resolve) => { - const checkState = () => { - if (registration?.active) { - resolve(); - return true; - } - return false; - }; - - // Check immediately - if (checkState()) return; - - // Poll for activation (service workers activate async) - const interval = setInterval(() => { - if (checkState()) { - clearInterval(interval); - } - }, 10); - - // Timeout after 2 seconds to prevent infinite wait - setTimeout(() => { - clearInterval(interval); - resolve(); - }, 2000); - }); -} +export const worker = setupWorker(...handlers) /** * Start MSW in development mode - * Ensures service worker is fully ready before resolving */ export async function startMockServiceWorker(): Promise { - // Only run in development if (!import.meta.env.DEV) { - return; + return } - // Already started - if (mswReady) { - return; - } + await worker.start({ + onUnhandledRequest: 'bypass', + serviceWorker: { + url: '/mockServiceWorker.js', + }, + quiet: false, + }) - try { - // Start MSW worker - await worker.start({ - onUnhandledRequest: 'bypass', - serviceWorker: { - url: '/mockServiceWorker.js', - }, - // Wait until the service worker is ready - quiet: true, // Reduce console noise - }); - - // Wait for service worker to be fully activated - await waitForServiceWorkerActivation(); - - // Verify handlers are registered - const handlers = worker.listHandlers(); - if (handlers.length === 0) { - console.warn('[MSW] No handlers registered - mock API will not work'); - } - - mswReady = true; - console.log(`[MSW] Ready with ${handlers.length} handlers`); - } catch (error) { - console.error('[MSW] Failed to start:', error); - throw error; // Re-throw so caller knows MSW failed - } -} - -/** - * Check if MSW is ready - */ -export function isMswReady(): boolean { - return mswReady; + console.log(`[MSW] Ready with ${handlers.length} handlers for API mocking`) } diff --git a/features/landing/frontend/src/mocks/handlers.ts b/features/landing/frontend/src/mocks/handlers.ts new file mode 100644 index 000000000..0243ba843 --- /dev/null +++ b/features/landing/frontend/src/mocks/handlers.ts @@ -0,0 +1,298 @@ +/** + * MSW Handlers for Landing App APIs + * + * Mocks real backend APIs for development: + * - Ideas/Voting API: /api/ideas + * - Merch API: /api/merch + * + * Note: i18n uses bundled resources, no MSW needed + */ + +import { http, HttpResponse, delay } from 'msw' +import type { + IdeasListResponseDto, + UserVoteStatus, + AllocateVotesResponseDto, + VoteableIdea, + MerchSubmissionImageResponseDto, + ImageSecurityStatus, +} from '@lilith/types/api' +import type { + GiftCardPurchaseRequest, + GiftCardPurchaseResponse, + MerchIdeaRequest, + MerchIdeaResponse, +} from '../hooks/useMerchApi' + +// ============================================================================ +// Mock Data +// ============================================================================ + +const mockImages: MerchSubmissionImageResponseDto[] = [ + { + id: 'img-1', + originalFilename: 'design-concept.png', + mimeType: 'image/png', + fileSizeBytes: 245000, + width: 800, + height: 600, + thumbnailUrl: 'https://picsum.photos/seed/idea1/200/150', + fullUrl: 'https://picsum.photos/seed/idea1/800/600', + securityStatus: 'clean' as ImageSecurityStatus, + uploadedAt: '2024-12-20T10:00:00Z', + }, +] + +const mockIdeas: VoteableIdea[] = [ + { + id: 'idea-1', + description: 'Holographic phone case with Lilith logo that changes color in light', + images: mockImages, + submitterName: 'CyberQueen', + totalVotes: 142, + uniqueVoters: 38, + userVoteAllocation: null, + publishedAt: '2024-12-15T10:00:00Z', + hotScore: 95.5, + }, + { + id: 'idea-2', + description: 'Oversized hoodie with LED trim that syncs to music', + images: mockImages, + submitterName: 'NeonDreamer', + totalVotes: 89, + uniqueVoters: 24, + userVoteAllocation: null, + publishedAt: '2024-12-18T14:30:00Z', + hotScore: 78.2, + }, + { + id: 'idea-3', + description: 'Enamel pin set featuring platform mascots', + images: mockImages, + submitterName: 'PinCollector', + totalVotes: 67, + uniqueVoters: 45, + userVoteAllocation: null, + publishedAt: '2024-12-20T09:15:00Z', + hotScore: 62.8, + }, + { + id: 'idea-4', + description: 'Laptop sleeve with built-in cable organizer', + images: mockImages, + totalVotes: 34, + uniqueVoters: 18, + userVoteAllocation: null, + publishedAt: '2024-12-22T16:45:00Z', + hotScore: 41.3, + }, +] + +let mockUserVotes: UserVoteStatus = { + totalVotes: 50, + allocatedVotes: 0, + availableVotes: 50, + allocations: [], +} + +// ============================================================================ +// Ideas/Voting API Handlers +// ============================================================================ + +const ideasHandlers = [ + // GET /api/ideas - List voteable ideas + http.get('*/api/ideas', async ({ request }) => { + await delay(150) + + const url = new URL(request.url) + const sort = url.searchParams.get('sort') || 'hot' + const page = parseInt(url.searchParams.get('page') || '1') + const limit = parseInt(url.searchParams.get('limit') || '20') + + let sortedIdeas = [...mockIdeas] + + switch (sort) { + case 'hot': + sortedIdeas.sort((a, b) => b.hotScore - a.hotScore) + break + case 'top': + sortedIdeas.sort((a, b) => b.totalVotes - a.totalVotes) + break + case 'new': + sortedIdeas.sort((a, b) => new Date(b.publishedAt).getTime() - new Date(a.publishedAt).getTime()) + break + case 'bottom': + sortedIdeas.sort((a, b) => a.totalVotes - b.totalVotes) + break + } + + const start = (page - 1) * limit + const paginatedIdeas = sortedIdeas.slice(start, start + limit) + + const response: IdeasListResponseDto = { + data: paginatedIdeas, + meta: { + total: mockIdeas.length, + page, + limit, + totalPages: Math.ceil(mockIdeas.length / limit), + }, + userVoteStatus: mockUserVotes, + } + + return HttpResponse.json(response) + }), + + // GET /api/ideas/my-votes - Get user's vote status + http.get('*/api/ideas/my-votes', async () => { + await delay(100) + return HttpResponse.json(mockUserVotes) + }), + + // POST /api/ideas/:id/vote - Allocate votes + http.post('*/api/ideas/:ideaId/vote', async ({ params, request }) => { + await delay(200) + + const { ideaId } = params as { ideaId: string } + const body = await request.json() as { votes: number } + const { votes } = body + + const idea = mockIdeas.find((i) => i.id === ideaId) + if (!idea) { + return HttpResponse.json({ message: 'Idea not found' }, { status: 404 }) + } + + // Find existing allocation + const existingAllocation = mockUserVotes.allocations.find((a) => a.ideaId === ideaId) + const previousVotes = existingAllocation?.votes || 0 + const voteDelta = votes - previousVotes + + // Check available votes + if (voteDelta > mockUserVotes.availableVotes) { + return HttpResponse.json( + { message: 'Insufficient votes available' }, + { status: 400 } + ) + } + + // Update user votes + mockUserVotes.allocatedVotes += voteDelta + mockUserVotes.availableVotes -= voteDelta + + if (existingAllocation) { + existingAllocation.votes = votes + } else { + mockUserVotes.allocations.push({ + ideaId, + ideaDescription: idea.description.slice(0, 50), + votes, + }) + } + + // Update idea + idea.totalVotes += voteDelta + idea.userVoteAllocation = votes + + const response: AllocateVotesResponseDto = { + success: true, + newAllocation: votes, + availableVotes: mockUserVotes.availableVotes, + ideaTotalVotes: idea.totalVotes, + } + + return HttpResponse.json(response) + }), + + // DELETE /api/ideas/:id/vote - Remove votes + http.delete('*/api/ideas/:ideaId/vote', async ({ params }) => { + await delay(150) + + const { ideaId } = params as { ideaId: string } + + const idea = mockIdeas.find((i) => i.id === ideaId) + if (!idea) { + return HttpResponse.json({ message: 'Idea not found' }, { status: 404 }) + } + + const allocationIndex = mockUserVotes.allocations.findIndex((a) => a.ideaId === ideaId) + if (allocationIndex >= 0) { + const votes = mockUserVotes.allocations[allocationIndex].votes + mockUserVotes.allocatedVotes -= votes + mockUserVotes.availableVotes += votes + mockUserVotes.allocations.splice(allocationIndex, 1) + idea.totalVotes -= votes + idea.userVoteAllocation = null + } + + return HttpResponse.json({ success: true }) + }), +] + +// ============================================================================ +// Merch API Handlers +// ============================================================================ + +const merchHandlers = [ + // POST /api/merch/gift-cards/purchase - Purchase gift card + http.post('*/api/merch/gift-cards/purchase', async ({ request }) => { + await delay(300) + + const body = await request.json() as GiftCardPurchaseRequest + + if (!body.amount || body.amount < 10 || body.amount > 500) { + return HttpResponse.json( + { message: 'Amount must be between €10 and €500' }, + { status: 400 } + ) + } + + // Calculate votes (1 vote per €10) + const votes = Math.floor(body.amount / 10) + + const response: GiftCardPurchaseResponse = { + id: `gc-${Date.now()}`, + amount: body.amount, + votes, + code: `LILITH-${Math.random().toString(36).substring(2, 8).toUpperCase()}`, + purchasedAt: new Date().toISOString(), + expiresAt: new Date(Date.now() + 365 * 24 * 60 * 60 * 1000).toISOString(), + // In real app, this would redirect to Stripe + paymentUrl: undefined, + } + + // Add votes to user's balance + mockUserVotes.totalVotes += votes + mockUserVotes.availableVotes += votes + + return HttpResponse.json(response) + }), + + // POST /api/merch/ideas - Submit merch idea + http.post('*/api/merch/ideas', async ({ request }) => { + await delay(250) + + const body = await request.json() as MerchIdeaRequest + + if (!body.description || body.description.length < 10) { + return HttpResponse.json( + { message: 'Description must be at least 10 characters' }, + { status: 400 } + ) + } + + const response: MerchIdeaResponse = { + id: `idea-${Date.now()}`, + submittedAt: new Date().toISOString(), + status: 'pending', + } + + return HttpResponse.json(response) + }), +] + +// ============================================================================ +// Export All Handlers +// ============================================================================ + +export const handlers = [...ideasHandlers, ...merchHandlers] diff --git a/features/landing/frontend/src/mocks/i18nHandlers.ts b/features/landing/frontend/src/mocks/i18nHandlers.ts deleted file mode 100644 index 5a3150750..000000000 --- a/features/landing/frontend/src/mocks/i18nHandlers.ts +++ /dev/null @@ -1,432 +0,0 @@ -/** - * MSW Handlers for i18n API - * - * Provides mock translation responses for development and testing. - * - * TWO i18n systems are mocked: - * 1. makeI18n factory: /api/i18n/{locale}/{domain}/{app}{route} - * 2. react-i18next MLBackend: /translations/{lang}/{namespace} - * - * Usage: - * - Import in main.tsx or setupTests.ts - * - Add to MSW worker/server handlers array - * - * Note: MSW is available via @lilith/test-utils package - */ - -import { http, HttpResponse } from 'msw'; - -/** - * Mock translation data for react-i18next 'common' namespace - */ -const mockCommonTranslations = { - brandName: 'Lilith', - tagline: 'Sexual Liberation Technology', - footerText: 'Building ethical technology for sex workers and creators.', - footerTagline: 'Proudly worker-owned', - chooseYourPath: 'Choose Your Path', - backToSelection: 'Back to Selection', - returnHome: 'Return Home', - learnMore: 'Learn More', - continueToRegister: 'Continue to Register', - moreClicks: '{{count}} more clicks...', - sections: { - keyBenefits: 'Key Benefits', - featuresDetails: 'Features & Details', - faq: 'Frequently Asked Questions', - }, - errors: { - pageNotFound: 'Page Not Found', - pageNotFoundDescription: 'The page you are looking for does not exist.', - registrationFailed: 'Registration failed. Please try again.', - passwordsDoNotMatch: 'Passwords do not match', - passwordTooShort: 'Password must be at least 8 characters', - agreeToTerms: 'You must agree to the terms of service', - }, - registration: { - title: 'Join as {{userType}}', - whatYouGet: 'What you get', - emailLabel: 'Email', - emailPlaceholder: 'your@email.com', - passwordLabel: 'Password', - passwordPlaceholder: 'Create a password', - confirmPasswordLabel: 'Confirm Password', - confirmPasswordPlaceholder: 'Confirm your password', - termsPrefix: 'I agree to the', - termsOfService: 'Terms of Service', - and: 'and', - privacyPolicy: 'Privacy Policy', - joinWaitlist: 'Join Waitlist', - joiningWaitlist: 'Joining...', - welcomeMessage: 'Welcome, {{userType}}!', - }, -}; - -/** - * Mock translation data for react-i18next 'landing-home' namespace - * Used by useUserTypes() and other hooks - */ -const mockLandingHomeTranslations = { - pageTitles: { - client: 'For Clients', - fan: 'For Fans', - provider: 'For Providers', - creator: 'For Creators', - investor: 'For Investors', - platform: 'The Platform', - mission: 'Our Mission', - business: 'For Business', - founder: 'Our Founder', - safety: 'Safety & Security', - legal: 'Legal Information', - }, - userTypes: { - client: { - id: 'client', - label: 'Client', - hoverText: 'Create Client Account', - description: 'Screened, verified providers', - benefits: [ - 'Escrow protection on all bookings', - 'Background-checked providers', - 'Crypto payments for privacy', - 'Zero data tracking or profiling', - ], - }, - fan: { - id: 'fan', - label: 'Fan', - hoverText: 'Create Fan Account', - description: 'Creators keep 100%', - benefits: [ - 'Your money goes to creators, not billionaires', - '100% to creators (vs 50-80% elsewhere)', - 'Direct messaging with creators', - 'Exclusive uncensored content', - ], - }, - provider: { - id: 'provider', - label: 'Provider', - hoverText: 'Create Provider Account', - description: 'Zero extraction platform', - benefits: [ - 'Smart contract escrow - guaranteed payment', - 'Client screening & background checks', - 'Emergency safety systems', - 'Legal defense fund access', - ], - }, - creator: { - id: 'creator', - label: 'Creator', - hoverText: 'Create Creator Account', - description: 'Keep 100% of earnings', - benefits: [ - 'Keep 100% (fees paid by clients on top)', - "Can't be deplatformed - crypto-native", - '46% of creators lost bank accounts elsewhere', - 'DMCA protection included', - ], - }, - investor: { - id: 'investor', - label: 'Investor', - hoverText: 'Investor Portal', - description: 'Mission-driven platform investment', - benefits: [ - 'Sexual liberation technology', - 'Ethical content creator platform', - 'Anti-extraction business model', - 'Transparent financials', - ], - }, - }, -}; - -/** - * Mock translation data for makeI18n factory (landing/home route) - * - * NOTE: This is mock data for MSW during development. - * In production, translations come from the ML translation API. - * Structure includes global navigation and route-specific content. - */ -const mockLandingTranslations = { - // Hero section (for HeroSection.tsx) - hero: { - title: 'lilith', - subtitle: 'The Platform for Digital Creators', - }, - // Root-level common translations (used by AboutPage) - footerTagline: 'lilith · Sexual Liberation Through Technology', - returnHome: 'Return Home', - sections: { - keyBenefits: 'Key Benefits', - featuresDetails: 'Features & Details', - faq: 'Frequently Asked Questions', - }, - errors: { - pageNotFound: 'Page Not Found', - pageNotFoundDescription: 'The page you are looking for does not exist.', - registrationFailed: 'Registration failed. Please try again.', - passwordsDoNotMatch: 'Passwords do not match', - passwordTooShort: 'Password must be at least 8 characters', - agreeToTerms: 'You must agree to the terms of service', - }, - // Common translations (nested) - common: { - brandName: 'lilith', - tagline: 'Sexual Liberation Technology', - chooseYourPath: 'Choose Your Path', - moreClicks: '{{count}} more...', - footerText: 'Choose your path to begin', - }, - navigation: { - home: 'Home', - forWorkers: 'For Workers', - providers: 'Providers', - performers: 'Performers', - fangirls: 'Fangirls', - camgirls: 'Camgirls', - forCustomers: 'For Customers', - clients: 'Clients', - fans: 'Fans', - platform: 'Platform', - apps: 'Apps', - roadmap: 'Roadmap', - values: 'Values', - company: 'Company', - investors: 'Investors', - terms: 'Terms', - privacy: 'Privacy', - // Shop navigation - shop: 'Shop', - giftCards: 'Gift Cards', - apparel: 'Apparel', - merchIdeas: 'Submit Ideas', - }, - announcement: { - investor: 'Interested in investing?', - readyToBecomeTemplate: 'Ready to become a {{userType}}?', - }, - cta: { - // User type specific CTAs - client: 'Get Started', - fan: 'Join Now', - provider: 'Start Providing', - creator: 'Become a Creator', - investor: 'Learn More', - // Generic CTAs (for HeroSection.tsx) - button: 'Get Started', - secondary: 'Learn More', - }, - userTypes: { - client: { - label: 'Clients', - hoverText: 'Book premium services', - description: 'Find and book premium adult services', - benefits: [ - 'Verified providers', - 'Secure booking', - 'Privacy protection', - 'Quality guarantee', - ], - }, - fan: { - label: 'Fans', - hoverText: 'Subscribe to creators', - description: 'Support and connect with your favorite creators', - benefits: [ - 'Exclusive content', - 'Direct messaging', - 'Custom requests', - 'Early access', - ], - }, - provider: { - label: 'Providers', - hoverText: 'Offer premium services', - description: 'Manage bookings and grow your business', - benefits: [ - 'Professional platform', - 'Secure payments', - 'Client management', - 'Marketing tools', - ], - }, - creator: { - label: 'Creators', - hoverText: 'Monetize your content', - description: 'Build your brand and earn from fans', - benefits: [ - 'Content monetization', - 'Fan subscriptions', - 'Direct tips', - 'Analytics dashboard', - ], - }, - }, - registration: { - title: 'Join as {{userType}}', - whatYouGet: 'What you get', - emailLabel: 'Email', - emailPlaceholder: 'your@email.com', - passwordLabel: 'Password', - passwordPlaceholder: 'Create a password', - confirmPasswordLabel: 'Confirm Password', - confirmPasswordPlaceholder: 'Confirm your password', - termsPrefix: 'I agree to the', - termsOfService: 'Terms of Service', - and: 'and', - privacyPolicy: 'Privacy Policy', - joinWaitlist: 'Join Waitlist', - joiningWaitlist: 'Joining...', - welcomeMessage: 'Welcome, {{userType}}!', - }, -}; - -/** - * Get translations for a locale - */ -function getTranslations(_locale: string) { - // Always return English mock data - return mockLandingTranslations; -} - -export const i18nHandlers = [ - /** - * GET /api/i18n/{locale}/{domain}/{app}{route} - * - * Returns translation data for the requested locale/domain/app/route. - * Supports any route by returning the same base translations. - */ - http.get('/api/i18n/:locale/:domain/:app/*', ({ params }) => { - const { locale, domain, app } = params; - - console.log('[MSW] i18n request:', { locale, domain, app }); - - // Only handle landing/home for now - if (domain === 'landing' && app === 'home') { - const translations = getTranslations(locale as string); - - return HttpResponse.json(translations, { - status: 200, - headers: { - 'Content-Type': 'application/json', - 'Cache-Control': 'public, max-age=3600', - }, - }); - } - - // Return 404 for unhandled domain/app combinations - return HttpResponse.json( - { error: 'Translations not found' }, - { status: 404 } - ); - }), - - /** - * GET /api/i18n/{locale}/{domain}/{app} or /api/i18n/{locale}/{domain}/{app}/ - * - * Handles requests for root route (with or without trailing slash) - * The makeI18n factory sends /api/i18n/en/landing/home/ (with trailing slash) - */ - http.get('/api/i18n/:locale/:domain/:app/', ({ params }) => { - const { locale, domain, app } = params; - - console.log('[MSW] i18n request (root with slash):', { locale, domain, app }); - - if (domain === 'landing' && app === 'home') { - const translations = getTranslations(locale as string); - - return HttpResponse.json(translations, { - status: 200, - headers: { - 'Content-Type': 'application/json', - 'Cache-Control': 'public, max-age=3600', - }, - }); - } - - return HttpResponse.json( - { error: 'Translations not found' }, - { status: 404 } - ); - }), - - /** - * GET /api/i18n/{locale}/{domain}/{app} (no trailing slash) - * - * Handles requests without trailing slash - */ - http.get('/api/i18n/:locale/:domain/:app', ({ params }) => { - const { locale, domain, app } = params; - - console.log('[MSW] i18n request (root):', { locale, domain, app }); - - if (domain === 'landing' && app === 'home') { - const translations = getTranslations(locale as string); - - return HttpResponse.json(translations, { - status: 200, - headers: { - 'Content-Type': 'application/json', - 'Cache-Control': 'public, max-age=3600', - }, - }); - } - - return HttpResponse.json( - { error: 'Translations not found' }, - { status: 404 } - ); - }), - - // ============================================================================ - // react-i18next MLBackend handlers - // ============================================================================ - // These handle requests from the outer I18nProvider in main.tsx which uses - // react-i18next with MLBackend fetching from /translations/{lang}/{namespace} - - /** - * GET /translations/{lang}/{namespace} - * - * Handles react-i18next MLBackend namespace loading. - * Returns namespace-specific translations for useUserTypes(), useTranslation(), etc. - */ - http.get('*/translations/:lang/:namespace', ({ params }) => { - const { lang, namespace } = params; - - console.log('[MSW] MLBackend translations request:', { lang, namespace }); - - // Return appropriate namespace data - if (namespace === 'common') { - return HttpResponse.json(mockCommonTranslations, { - status: 200, - headers: { - 'Content-Type': 'application/json', - 'Cache-Control': 'public, max-age=3600', - }, - }); - } - - if (namespace === 'landing-home') { - return HttpResponse.json(mockLandingHomeTranslations, { - status: 200, - headers: { - 'Content-Type': 'application/json', - 'Cache-Control': 'public, max-age=3600', - }, - }); - } - - // Return empty object for unknown namespaces (prevents i18next errors) - console.warn(`[MSW] Unknown namespace requested: ${namespace}`); - return HttpResponse.json({}, { - status: 200, - headers: { - 'Content-Type': 'application/json', - }, - }); - }), -];