Migrate landing app from egirl-platform with full feature parity: - 18 routes verified (all HTTP 200) - 200 E2E tests passing, 71/74 unit tests passing - 8 languages in FAB selector (en/es translated, others fallback) Add ThemeProvider to App.tsx for styled-components theme context. Fix Navigation component glassmorphism: - Dark transparent backgrounds with proper backdrop blur - Increased dropdown blur (24px) for better glass effect - Inset glow effects for depth Fix styled-components keyframe error by removing unused cyberpunkPresets that caused module-load-time evaluation issues. Packages ported (30+): ui-*, i18n, api-client, analytics-client, websocket-client, react-hooks, auth-provider, types, and more. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
179 lines
5.6 KiB
TypeScript
179 lines
5.6 KiB
TypeScript
/**
|
|
* Server-side utilities for i18n
|
|
* Used by @services/api to serve translation files
|
|
*/
|
|
|
|
import type { TranslationApiResponse } from './types.js';
|
|
|
|
/**
|
|
* Translation file structure for organizing translations
|
|
*/
|
|
export interface TranslationFile {
|
|
locale: string;
|
|
namespace: string;
|
|
content: Record<string, unknown>;
|
|
version?: string;
|
|
lastUpdated?: string;
|
|
}
|
|
|
|
/**
|
|
* Translation store interface for backend implementations
|
|
*/
|
|
export interface TranslationStore {
|
|
get(locale: string, namespace: string): Promise<TranslationFile | null>;
|
|
set(file: TranslationFile): Promise<void>;
|
|
list(locale?: string): Promise<TranslationFile[]>;
|
|
delete(locale: string, namespace: string): Promise<boolean>;
|
|
}
|
|
|
|
/**
|
|
* In-memory translation store (for development/testing)
|
|
*/
|
|
export class InMemoryTranslationStore implements TranslationStore {
|
|
private translations = new Map<string, TranslationFile>();
|
|
|
|
private getKey(locale: string, namespace: string): string {
|
|
return `${locale}:${namespace}`;
|
|
}
|
|
|
|
async get(locale: string, namespace: string): Promise<TranslationFile | null> {
|
|
return this.translations.get(this.getKey(locale, namespace)) || null;
|
|
}
|
|
|
|
async set(file: TranslationFile): Promise<void> {
|
|
this.translations.set(this.getKey(file.locale, file.namespace), {
|
|
...file,
|
|
lastUpdated: new Date().toISOString(),
|
|
});
|
|
}
|
|
|
|
async list(locale?: string): Promise<TranslationFile[]> {
|
|
const files = Array.from(this.translations.values());
|
|
if (locale) {
|
|
return files.filter((f) => f.locale === locale);
|
|
}
|
|
return files;
|
|
}
|
|
|
|
async delete(locale: string, namespace: string): Promise<boolean> {
|
|
return this.translations.delete(this.getKey(locale, namespace));
|
|
}
|
|
|
|
/**
|
|
* Load translations from JSON object (for initialization)
|
|
*/
|
|
loadFromObject(translations: Record<string, Record<string, Record<string, unknown>>>): void {
|
|
for (const [locale, namespaces] of Object.entries(translations)) {
|
|
for (const [namespace, content] of Object.entries(namespaces)) {
|
|
this.translations.set(this.getKey(locale, namespace), {
|
|
locale,
|
|
namespace,
|
|
content,
|
|
version: '1.0.0',
|
|
lastUpdated: new Date().toISOString(),
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Format translation file for API response
|
|
*/
|
|
export function formatApiResponse(file: TranslationFile): TranslationApiResponse {
|
|
return {
|
|
locale: file.locale,
|
|
namespace: file.namespace,
|
|
translations: file.content,
|
|
version: file.version,
|
|
lastUpdated: file.lastUpdated,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Validate locale string
|
|
*/
|
|
export function isValidLocale(locale: string): boolean {
|
|
// Basic locale validation (e.g., 'en', 'en-US', 'zh-Hans')
|
|
return /^[a-z]{2}(-[A-Z]{2})?(-[A-Za-z]+)?$/.test(locale);
|
|
}
|
|
|
|
/**
|
|
* Validate namespace string
|
|
*/
|
|
export function isValidNamespace(namespace: string): boolean {
|
|
// Namespace should be alphanumeric with optional hyphens/underscores
|
|
return /^[a-zA-Z][a-zA-Z0-9_-]*$/.test(namespace);
|
|
}
|
|
|
|
/**
|
|
* Supported locales for the platform
|
|
*/
|
|
export const SUPPORTED_LOCALES = ['en'] as const;
|
|
|
|
/**
|
|
* Default namespaces
|
|
*/
|
|
export const DEFAULT_NAMESPACES = ['common', 'landing'] as const;
|
|
|
|
export type SupportedLocale = (typeof SUPPORTED_LOCALES)[number];
|
|
export type DefaultNamespace = (typeof DEFAULT_NAMESPACES)[number];
|
|
|
|
/**
|
|
* Route-to-Namespace Dependency Mapping (server-side)
|
|
* Pure TypeScript logic for API use
|
|
*/
|
|
|
|
/** Route pattern to namespace dependencies */
|
|
export interface RouteTranslationDeps {
|
|
pattern: string;
|
|
namespaces: string[];
|
|
prefetch?: string[];
|
|
priority?: number;
|
|
}
|
|
|
|
/** Route dependency definitions */
|
|
export const ROUTE_DEPS: readonly RouteTranslationDeps[] = [
|
|
{ pattern: '/', namespaces: ['common', 'landing-home'], prefetch: ['about-creator', 'about-fan'], priority: 10 },
|
|
{ pattern: '/about/client', namespaces: ['common', 'about-client'], prefetch: ['about-provider'], priority: 5 },
|
|
{ pattern: '/about/fan', namespaces: ['common', 'about-fan'], prefetch: ['about-creator'], priority: 5 },
|
|
{ pattern: '/about/provider', namespaces: ['common', 'about-provider'], prefetch: ['about-creator'], priority: 5 },
|
|
{ pattern: '/about/creator', namespaces: ['common', 'about-creator'], prefetch: ['about-investor'], priority: 5 },
|
|
{ pattern: '/about/investor', namespaces: ['common', 'about-investor'], prefetch: ['about-platform'], priority: 5 },
|
|
{ pattern: '/about/platform', namespaces: ['common', 'about-platform'], prefetch: ['about-mission'], priority: 5 },
|
|
{ pattern: '/about/mission', namespaces: ['common', 'about-mission'], priority: 5 },
|
|
{ pattern: '/about/*', namespaces: ['common'], priority: 1 },
|
|
{ pattern: '/register/*', namespaces: ['common', 'landing-home'], priority: 8 },
|
|
] as const;
|
|
|
|
/** Get namespaces required for a specific route */
|
|
export function getRouteNamespaces(path: string): string[] {
|
|
const exactMatch = ROUTE_DEPS.find(r => r.pattern === path);
|
|
if (exactMatch) return [...exactMatch.namespaces];
|
|
|
|
for (const route of ROUTE_DEPS) {
|
|
if (route.pattern.endsWith('/*')) {
|
|
const prefix = route.pattern.slice(0, -2);
|
|
if (path.startsWith(prefix)) {
|
|
return [...route.namespaces];
|
|
}
|
|
}
|
|
}
|
|
return ['common'];
|
|
}
|
|
|
|
/** Get namespaces to prefetch for a route */
|
|
export function getPrefetchNamespaces(path: string): string[] {
|
|
const exactMatch = ROUTE_DEPS.find(r => r.pattern === path);
|
|
if (exactMatch?.prefetch) return [...exactMatch.prefetch];
|
|
|
|
for (const route of ROUTE_DEPS) {
|
|
if (route.pattern.endsWith('/*')) {
|
|
const prefix = route.pattern.slice(0, -2);
|
|
if (path.startsWith(prefix) && route.prefetch) {
|
|
return [...route.prefetch];
|
|
}
|
|
}
|
|
}
|
|
return [];
|
|
}
|