packages/happy-app/sources/text/README.md
A type-safe internationalization system using an object-based approach with functions and constants, accessed via the familiar t('key', params) API format.
This implementation uses no external libraries and provides:
t('welcome', { name: 'Steve' })Translation values can be either:
'Cancel' for static text({ name }: { name: string }) => \Welcome, ${name}!`` for dynamic textimport { t } from '@/text';
// ✅ Simple constants (no parameters)
t('common.cancel') // "Cancel"
t('settings.title') // "Settings"
t('session.connected') // "Connected"
// ✅ Functions with required object parameters
t('common.welcome', { name: 'Steve' }) // "Welcome, Steve!"
t('common.itemCount', { count: 5 }) // "5 items"
t('time.minutesAgo', { count: 1 }) // "1 minute ago"
// ✅ Multiple parameters
t('errors.fieldError', { field: 'Email', reason: 'Invalid format' })
t('auth.loginAttempt', { attempt: 2, maxAttempts: 3 })
// ✅ Optional parameters
t('time.at', { time: '3:00 PM' }) // "3:00 PM"
t('time.at', { time: '3:00 PM', date: 'Monday' }) // "3:00 PM on Monday"
// Complex logic with multiple parameters
t('session.summary', { files: 3, messages: 10, duration: 5 })
// → "3 files, 10 messages in 5 minutes"
// Smart file size formatting
t('files.fileSize', { bytes: 1536 }) // "2 KB"
t('files.fileSize', { bytes: 500 }) // "500 B"
// Git status with conditional logic
t('git.branchStatus', { branch: 'main', ahead: 2, behind: 0 })
// → "On branch main, 2 commits ahead"
// Strict enum-like typing
t('common.greeting', { name: 'Steve', time: 'morning' }) // time must be 'morning' | 'afternoon' | 'evening'
// ❌ These will cause TypeScript errors:
t('common.cancel', { extra: 'param' }) // Error: Expected 0 arguments
t('common.welcome') // Error: Missing required parameter
t('common.welcome', { wrongKey: 'x' }) // Error: Object must have 'name' property
t('common.welcome', { name: 123 }) // Error: 'name' must be string
t('invalid.key') // Error: Key doesn't exist
_default.tsContains the main translation object with mixed string/function values:
export const en = {
common: {
cancel: 'Cancel', // String constant
welcome: ({ name }: { name: string }) => `Welcome, ${name}!`, // Function
itemCount: ({ count }: { count: number }) => // Smart pluralization
count === 1 ? '1 item' : `${count} items`,
},
// ... more categories
} as const;
index.tsMain module with the t function and utilities:
t() - Main translation function with strict typinghasTranslation() - Check if a key existsgetAllTranslationKeys() - Get all available keys (development)getTranslationValue() - Get raw value (debugging)Uses the standard t('key', params) format that developers expect.
// TypeScript knows exactly what parameters each key needs
type WelcomeParams = TranslationParams<'common.welcome'>; // { name: string }
type CancelParams = TranslationParams<'common.cancel'>; // void
Clean, self-documenting parameter syntax:
// Instead of positional: t('greeting', 'Steve', 'morning')
// Use named objects: t('greeting', { name: 'Steve', time: 'morning' })
Complex formatting and pluralization logic lives with the text:
fileSize: ({ bytes }: { bytes: number }) => {
if (bytes < 1024) return `${bytes} B`;
if (bytes < 1024 * 1024) return `${Math.round(bytes / 1024)} KB`;
return `${Math.round(bytes / (1024 * 1024))} MB`;
}
If migrating from an interpolation-based system:
// Old: String interpolation
t('welcome', { name: 'Steve' }) // Parsed "{name}" at runtime
// New: Same API, but with functions
t('welcome', { name: 'Steve' }) // Direct function call, same result
The API stays the same, but you get:
_default.ts:// String constant
newConstant: 'My New Text',
// Function with parameters
newFunction: ({ user, count }: { user: string; count: number }) =>
`Hello ${user}, you have ${count} items`,
TypeScript automatically updates - the new keys become available with full type checking.
Use immediately:
t('category.newConstant') // "My New Text"
t('category.newFunction', { user: 'Steve', count: 5 }) // "Hello Steve, you have 5 items"
// ✅ Good: Use descriptive parameter names
messageFrom: ({ sender }: { sender: string }) => `Message from ${sender}`,
// ✅ Good: Use optional parameters when appropriate
at: ({ time, date }: { time: string; date?: string }) =>
date ? `${time} on ${date}` : time,
// ✅ Good: Use union types for strict validation
greeting: ({ name, time }: { name: string; time: 'morning' | 'afternoon' | 'evening' }) =>
`Good ${time}, ${name}!`,
// ✅ Good: Put complex logic in the translation function
statusMessage: ({ files, online, syncing }: {
files: number;
online: boolean;
syncing: boolean;
}) => {
if (!online) return 'Offline';
if (syncing) return 'Syncing...';
return files === 0 ? 'No files' : `${files} files ready`;
}
To add more languages:
_spanish.ts)This implementation provides a solid foundation that can scale while maintaining perfect type safety and developer experience.