platform-codebase/@packages/@infrastructure/i18n/src/server.ts
Quinn Ftw 84d1333284 feat(landing): complete migration with glassmorphism navigation
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>
2025-12-26 17:11:07 -08:00

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 [];
}