--- name: "i18n-translation-guide" description: "Implement internationalization (i18n) in Ballee using react-i18next with Trans component and useTranslation hook; use when adding user-facing text, translating components, implementing toast messages, or organizing translation files" version: "1.0.0" last_updated: "2025-12-03" --- # i18n Translation Guide **Purpose**: Comprehensive guide for implementing internationalization (i18n) in Ballee using react-i18next **When to Use**: When adding user-facing text, creating UI components, implementing toast messages, or translating existing hardcoded text --- ## Overview Ballee uses **react-i18next** with the **Trans component** from `@kit/ui/trans` for all internationalization. This skill provides battle-tested patterns from translating 100+ strings across marketing pages and admin dialogs. ### Framework Stack - **Library**: react-i18next + i18next - **Component**: `` from `@kit/ui/trans` - **Hook**: `useTranslation()` from `react-i18next` - **Translation Files**: `apps/web/public/locales/en/*.json` --- ## Core Patterns ### Pattern 1: UI Components (JSX) Use `` component for all JSX elements (titles, labels, buttons, etc.) ```tsx import { Trans } from '@kit/ui/trans'; // Simple text

// With variable interpolation // Translation: "Are you sure you want to delete "{{name}}"?" ``` **When to Use**: - ✅ Dialog titles/descriptions - ✅ Form labels - ✅ Button text - ✅ Page headings - ✅ Empty state messages - ✅ Table column headers ### Pattern 2: Toast Messages (Strings) ⚠️ **CRITICAL**: Toast messages require the `useTranslation` hook because they expect strings, not JSX. ```tsx import { useTranslation } from 'react-i18next'; import { toast } from '@kit/ui/sonner'; function MyComponent() { const { t } = useTranslation(); const handleAction = async () => { // ❌ WRONG - Toast doesn't accept JSX toast.success(); // ✅ CORRECT - Use t() function toast.success(t('admin:clients.toast.created')); // With interpolation toast.loading(t('admin:clients.toast.switchingTo', { name: clientName })); }; } ``` **When to Use `t()` instead of ``**: - ✅ Toast messages (success, error, loading) - ✅ Input placeholders (`placeholder={t('...')}`) - ✅ Accessibility attributes (`aria-label={t('...')}`) - ✅ Any prop that expects a string, not JSX ### Pattern 3: Server Components For Next.js server components, use `withI18n` HOC wrapper: ```tsx import { withI18n } from '~/lib/i18n/with-i18n'; import { Trans } from '@kit/ui/trans'; function ServerPage() { return (
); } export default withI18n(ServerPage); ``` --- ## Translation File Organization ### Namespace Structure Follow hierarchical naming: `namespace:category.subcategory.key` ```json // apps/web/public/locales/en/admin.json { "clients": { "title": "Clients", "createDialog": { "title": "Create Client", "description": "Add a new client account..." }, "form": { "name": "Name", "slug": "Slug", "email": "Email" }, "buttons": { "create": "Create Client", "creating": "Creating...", "save": "Save Changes", "saving": "Saving..." }, "toast": { "created": "Client created successfully", "createFailed": "Failed to create client", "switchingTo": "Switching to {{name}}...", "switchedTo": "Switched to {{name}}" }, "table": { "name": "Name", "slug": "Slug", "email": "Email", "actions": "Actions", "searchPlaceholder": "Search clients..." }, "actions": { "edit": "Edit", "delete": "Delete", "openMenu": "Open menu" }, "empty": { "title": "No clients yet", "description": "Create your first client to get started" }, "confirmDialog": { "delete": { "title": "Delete Client", "description": "Are you sure you want to delete \"{{name}}\"?..." } } } } ``` ### Namespace Guidelines **common.json** - Shared UI elements across the app ```json { "buttons": { "create": "Create", "creating": "Creating...", "save": "Save", "saving": "Saving...", "delete": "Delete", "deleting": "Deleting...", "cancel": "Cancel", "confirm": "Confirm", "retry": "Retry", "close": "Close" }, "form": { "name": "Name", "email": "Email", "notes": "Notes", "notesOptional": "Notes (optional)", "description": "Description" }, "dialog": { "confirm": "Are you sure?", "warning": "This action cannot be undone" }, "toast": { "deleteSuccess": "Deleted successfully", "updateSuccess": "Updated successfully" }, "empty": { "noItems": "No items found", "noResults": "No results found" }, "loading": { "default": "Loading...", "data": "Loading data..." }, "accessibility": { "closeDialog": "Close dialog", "openMenu": "Open menu" } } ``` **marketing.json** - Public marketing pages ```json { "home": { "hero": {...}, "features": {...} }, "contact": { "hero": {...}, "methods": {...}, "form": {...} }, "about": { "hero": {...}, "story": {...}, "values": {...} }, "services": { "hero": {...}, "list": {...}, "whyChooseUs": {...} } } ``` **admin.json** - Admin section (super admin) ```json { "common": { "buttons": {...}, "dialog": {...} }, "clients": { "createDialog": {...}, "table": {...}, "toast": {...} }, "events": { "cast": {...}, "table": {...}, "toast": {...} }, "productions": { "form": {...}, "detail": {...} } } ``` **user.json** - Dancer/user section ```json { "events": { "participation": {...}, "detail": {...} }, "assignments": { "contract": {...} }, "reimbursements": { "detail": {...}, "dialog": {...} } } ``` --- ## Common UI Patterns ### Empty States ```tsx // Pattern: Icon + Title + Description

``` ### Form Fields ```tsx ( The display name of the client {/* ⚠️ Consider: translate or leave? */} )} /> ``` **Form Field Translation Considerations**: - **Labels**: Always translate (``) - **Placeholders**: Optional - depends on whether they're example data or instructional - Example data (e.g., "Fever", "john@example.com"): Leave hardcoded - Instructions (e.g., "Enter your email"): Translate - **FormDescription**: Optional - translate if it's important user guidance ### Conditional Button States ```tsx ``` ### Table Columns with Translation When defining table columns, pass the `t` function as a parameter: ```tsx import { useTranslation } from 'react-i18next'; function MyTable() { const { t } = useTranslation(); const columns = getColumns(t, ...otherParams); return ; } function getColumns( t: (key: string) => string, // ...other params ): ColumnDef[] { return [ { id: 'name', header: t('admin:clients.table.name'), cell: ({ row }) => (
{row.original.name} {isActive && ( {t('admin:clients.table.active')} )}
), }, { id: 'actions', header: t('admin:clients.table.actions'), cell: ({ row }) => ( {t('admin:clients.actions.openMenu')} {t('admin:clients.actions.edit')} {t('admin:clients.actions.delete')} ), }, ]; } ``` ### Dialog Patterns ```tsx {/* Form content */} ``` ### Confirmation Dialogs ```tsx {isDeleting ? ( ) : ( )} ``` --- ## Accessibility Text Always translate sr-only text for screen readers: ```tsx ``` --- ## Search Input Placeholders Use the `t()` function for placeholders: ```tsx import { useTranslation } from 'react-i18next'; function MyTable() { const { t } = useTranslation(); return ( ); } ``` --- ## Variable Interpolation ### Simple Variables ```tsx // Translation: "Switched to {{name}}" // Output: "Switched to Fever" ``` ### Multiple Variables ```tsx // Translation: "Event on {{date}} at {{location}}" // Output: "Event on 2025-11-23 at Opera House" ``` ### With t() Function ```tsx const { t } = useTranslation(); toast.success(t('admin:clients.toast.switchedTo', { name: clientName })); ``` --- ## Migration Checklist When translating existing components: ### Step 1: Identify Hardcoded Text - [ ] Dialog titles/descriptions - [ ] Form labels - [ ] Button text (including conditional states: "Creating...", "Create") - [ ] Toast messages - [ ] Table column headers - [ ] Empty state messages - [ ] Search placeholders - [ ] Accessibility text (sr-only) - [ ] Action menu items ### Step 2: Add Imports ```tsx // For UI components import { Trans } from '@kit/ui/trans'; // For toast messages, placeholders, table columns import { useTranslation } from 'react-i18next'; ``` ### Step 3: Add Translations to JSON Files Choose the appropriate namespace: - `common.json` - Shared UI (buttons, forms, dialogs) - `admin.json` - Admin-specific features - `marketing.json` - Public marketing pages - `user.json` - Dancer/user features ### Step 4: Replace Hardcoded Text ```tsx // Before Create Client toast.success('Client created successfully'); // After const { t } = useTranslation(); toast.success(t('admin:clients.toast.created')); ``` ### Step 5: Validate JSON ```bash # Validate JSON syntax node -e "JSON.parse(require('fs').readFileSync('apps/web/public/locales/en/admin.json', 'utf8'))" ``` ### Step 6: Test Build ```bash pnpm build # Ensure no translation errors ``` --- ## Anti-Patterns (Don't Do This) ### ❌ Using Trans for Toast Messages ```tsx // ❌ WRONG - Toast expects strings, not JSX toast.success(); // ✅ CORRECT const { t } = useTranslation(); toast.success(t('admin:clients.toast.created')); ``` ### ❌ Hardcoding Text in Shared Components ```tsx // ❌ WRONG // ✅ CORRECT - Use common namespace ``` ### ❌ Duplicating Translations ```tsx // ❌ WRONG - Duplicate "Cancel" across namespaces // admin.json: { "buttons": { "cancel": "Cancel" } } // user.json: { "buttons": { "cancel": "Cancel" } } // ✅ CORRECT - Share common translations ``` ### ❌ Not Translating Accessibility Text ```tsx // ❌ WRONG Open menu // ✅ CORRECT {t('admin:clients.actions.openMenu')} ``` ### ❌ Using Defaults Instead of JSON Files ```tsx // ❌ WRONG - Defaults bypass translation system // ✅ CORRECT - Add to JSON file, no defaults // admin.json: { "clients": { "title": "Clients" } } ``` --- ## Quick Reference | Use Case | Pattern | Example | |----------|---------|---------| | UI Text | `` | `` | | Toast Messages | `t()` | `toast.success(t('admin:clients.toast.created'))` | | Button States | Conditional `` | `{isLoading ? : }` | | Table Headers | `t()` in column def | `header: t('admin:clients.table.name')` | | Placeholders | `t()` in prop | `placeholder={t('...')}` | | Accessibility | `t()` in sr-only | `{t('...')}` | | Variables | `values` prop | `` | --- ## Examples from Production ### Marketing Page (Homepage) ```tsx import { Trans } from '@kit/ui/trans'; import { withI18n } from '~/lib/i18n/with-i18n'; function HomePage() { return ( } headline={} subheadline={} /> ); } export default withI18n(HomePage); ``` ### Admin Dialog (Client Creation) ```tsx import { useTranslation } from 'react-i18next'; import { Trans } from '@kit/ui/trans'; export function CreateClientDialog({ children }) { const { t } = useTranslation(); const onSubmit = async (data) => { const result = await createClientAction(data); if (result.error) { toast.error(t('admin:clients.toast.createFailed'), { description: result.error, }); return; } toast.success(t('admin:clients.toast.created')); }; return (
); } ``` ### Data Table with Translations ```tsx import { useTranslation } from 'react-i18next'; import { Trans } from '@kit/ui/trans'; export function ClientsTable({ clients }) { const { t } = useTranslation(); const columns = getColumns(t, handleEdit, handleDelete); if (clients.length === 0) { return (

); } return ( <> ); } function getColumns(t, onEdit, onDelete) { return [ { id: 'name', header: t('admin:clients.table.name'), cell: ({ row }) => row.original.name, }, { id: 'actions', header: t('admin:clients.table.actions'), cell: ({ row }) => ( {t('admin:clients.actions.openMenu')} onEdit(row.original)}> {t('admin:clients.actions.edit')} onDelete(row.original)}> {t('admin:clients.actions.delete')} ), }, ]; } ``` --- ## Testing Translations ### Manual Testing 1. Check UI in browser - all text should render 2. Test toast messages - verify they show string content 3. Test empty states - verify messages display 4. Test table columns - verify headers show 5. Test button states - verify conditional text works ### Build Validation ```bash pnpm build # Ensure no missing translation errors pnpm typecheck # Ensure types are correct ``` ### Translation Key Validation ```bash # Check for unused translation keys grep -r "i18nKey" apps/web/app | grep -o '"[^"]*"' | sort | uniq # Check for missing translations # (manual comparison with JSON files) ``` --- ## Future Enhancements 1. **FormDescription Translation**: Decide whether to translate helper text under form fields 2. **Placeholder Translation**: Create guidelines for when to translate vs. leave hardcoded 3. **Dynamic Content**: Pattern for translating database-driven content 4. **Pluralization**: Handle singular/plural forms (e.g., "1 item" vs "5 items") 5. **Date/Number Formatting**: Locale-specific formatting patterns 6. **RTL Support**: Right-to-left language support guidelines --- ## Summary **Quick Start**: 1. Import `` for JSX, `useTranslation()` for strings 2. Add translations to appropriate namespace (`common`, `admin`, `marketing`, `user`) 3. Use `` for UI components 4. Use `t('...')` for toast messages, placeholders, and string props 5. Always translate accessibility text 6. Test build and UI thoroughly **Key Learnings**: - Toast messages need `t()`, not `` - Table columns should use `t()` function passed as parameter - Button states need conditional `` components - Shared UI elements belong in `common.json` - Variable interpolation uses `values` prop **Production Stats** (as of 2025-11-23): - ✅ 100+ strings translated across marketing pages and admin dialogs - ✅ Pattern validated across 8 TSX files - ✅ 3 namespaces fully structured (common, marketing, admin)