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',
- },
- });
- }),
-];