skills/ballee/i18n-translation-guide/SKILL.md
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
npx skillsauth add javeedishaq/ai-workflow-orchestrator i18n-translation-guideInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
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
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.
<Trans> from @kit/ui/transuseTranslation() from react-i18nextapps/web/public/locales/en/*.jsonUse <Trans> component for all JSX elements (titles, labels, buttons, etc.)
import { Trans } from '@kit/ui/trans';
// Simple text
<h1><Trans i18nKey="marketing:home.hero.headline" /></h1>
// With variable interpolation
<Trans
i18nKey="admin:clients.confirmDialog.delete.description"
values={{ name: clientName }}
/>
// Translation: "Are you sure you want to delete "{{name}}"?"
When to Use:
⚠️ CRITICAL: Toast messages require the useTranslation hook because they expect strings, not JSX.
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(<Trans i18nKey="admin:clients.toast.created" />);
// ✅ 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 <Trans>:
placeholder={t('...')})aria-label={t('...')})For Next.js server components, use withI18n HOC wrapper:
import { withI18n } from '~/lib/i18n/with-i18n';
import { Trans } from '@kit/ui/trans';
function ServerPage() {
return (
<div>
<Trans i18nKey="marketing:home.hero.headline" />
</div>
);
}
export default withI18n(ServerPage);
Follow hierarchical naming: namespace:category.subcategory.key
// 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}}\"?..."
}
}
}
}
common.json - Shared UI elements across the app
{
"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
{
"home": { "hero": {...}, "features": {...} },
"contact": { "hero": {...}, "methods": {...}, "form": {...} },
"about": { "hero": {...}, "story": {...}, "values": {...} },
"services": { "hero": {...}, "list": {...}, "whyChooseUs": {...} }
}
admin.json - Admin section (super admin)
{
"common": { "buttons": {...}, "dialog": {...} },
"clients": { "createDialog": {...}, "table": {...}, "toast": {...} },
"events": { "cast": {...}, "table": {...}, "toast": {...} },
"productions": { "form": {...}, "detail": {...} }
}
user.json - Dancer/user section
{
"events": { "participation": {...}, "detail": {...} },
"assignments": { "contract": {...} },
"reimbursements": { "detail": {...}, "dialog": {...} }
}
// Pattern: Icon + Title + Description
<div className="py-8 text-center">
<Building2 className="text-muted-foreground mx-auto mb-4 h-12 w-12" />
<h3 className="text-muted-foreground text-lg font-medium">
<Trans i18nKey="admin:clients.empty.title" />
</h3>
<p className="text-muted-foreground mt-2 text-sm">
<Trans i18nKey="admin:clients.empty.description" />
</p>
</div>
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>
<Trans i18nKey="admin:clients.form.name" />
</FormLabel>
<FormControl>
<Input
placeholder="Fever" // ⚠️ Consider: translate or leave hardcoded?
{...field}
/>
</FormControl>
<FormDescription>
The display name of the client {/* ⚠️ Consider: translate or leave? */}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
Form Field Translation Considerations:
<Trans i18nKey="..." />)<Button type="submit" disabled={isSubmitting}>
{isSubmitting ? (
<Trans i18nKey="admin:clients.buttons.creating" />
) : (
<Trans i18nKey="admin:clients.buttons.create" />
)}
</Button>
When defining table columns, pass the t function as a parameter:
import { useTranslation } from 'react-i18next';
function MyTable() {
const { t } = useTranslation();
const columns = getColumns(t, ...otherParams);
return <DataTable columns={columns} data={data} />;
}
function getColumns(
t: (key: string) => string,
// ...other params
): ColumnDef<MyType>[] {
return [
{
id: 'name',
header: t('admin:clients.table.name'),
cell: ({ row }) => (
<div>
{row.original.name}
{isActive && (
<Badge>
{t('admin:clients.table.active')}
</Badge>
)}
</div>
),
},
{
id: 'actions',
header: t('admin:clients.table.actions'),
cell: ({ row }) => (
<DropdownMenu>
<DropdownMenuTrigger>
<span className="sr-only">{t('admin:clients.actions.openMenu')}</span>
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem>{t('admin:clients.actions.edit')}</DropdownMenuItem>
<DropdownMenuItem>{t('admin:clients.actions.delete')}</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
),
},
];
}
<Dialog open={open} onOpenChange={setOpen}>
<DialogContent>
<DialogHeader>
<DialogTitle>
<Trans i18nKey="admin:clients.createDialog.title" />
</DialogTitle>
<DialogDescription>
<Trans i18nKey="admin:clients.createDialog.description" />
</DialogDescription>
</DialogHeader>
{/* Form content */}
<DialogFooter>
<Button variant="outline" onClick={() => setOpen(false)}>
<Trans i18nKey="common:buttons.cancel" />
</Button>
<Button type="submit" disabled={isSubmitting}>
{isSubmitting ? (
<Trans i18nKey="admin:clients.buttons.saving" />
) : (
<Trans i18nKey="admin:clients.buttons.save" />
)}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
<AlertDialog open={!!itemToDelete} onOpenChange={...}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>
<Trans i18nKey="admin:clients.confirmDialog.delete.title" />
</AlertDialogTitle>
<AlertDialogDescription>
<Trans
i18nKey="admin:clients.confirmDialog.delete.description"
values={{ name: itemToDelete?.name }}
/>
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel disabled={isDeleting}>
<Trans i18nKey="common:buttons.cancel" />
</AlertDialogCancel>
<AlertDialogAction
onClick={handleDelete}
disabled={isDeleting}
className="bg-destructive text-destructive-foreground"
>
{isDeleting ? (
<Trans i18nKey="common:buttons.deleting" />
) : (
<Trans i18nKey="common:buttons.delete" />
)}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
Always translate sr-only text for screen readers:
<Button variant="ghost" size="sm">
<MoreHorizontal className="h-4 w-4" />
<span className="sr-only">{t('admin:clients.actions.openMenu')}</span>
</Button>
Use the t() function for placeholders:
import { useTranslation } from 'react-i18next';
function MyTable() {
const { t } = useTranslation();
return (
<TableSearchInput
defaultValue={filters.search}
placeholder={t('admin:clients.table.searchPlaceholder')}
className="w-64"
/>
);
}
<Trans
i18nKey="admin:clients.toast.switchedTo"
values={{ name: clientName }}
/>
// Translation: "Switched to {{name}}"
// Output: "Switched to Fever"
<Trans
i18nKey="admin:events.detail.info"
values={{ date: eventDate, location: venueName }}
/>
// Translation: "Event on {{date}} at {{location}}"
// Output: "Event on 2025-11-23 at Opera House"
const { t } = useTranslation();
toast.success(t('admin:clients.toast.switchedTo', { name: clientName }));
When translating existing components:
// For UI components
import { Trans } from '@kit/ui/trans';
// For toast messages, placeholders, table columns
import { useTranslation } from 'react-i18next';
Choose the appropriate namespace:
common.json - Shared UI (buttons, forms, dialogs)admin.json - Admin-specific featuresmarketing.json - Public marketing pagesuser.json - Dancer/user features// Before
<DialogTitle>Create Client</DialogTitle>
<Button>{isSubmitting ? 'Creating...' : 'Create Client'}</Button>
toast.success('Client created successfully');
// After
<DialogTitle><Trans i18nKey="admin:clients.createDialog.title" /></DialogTitle>
<Button>
{isSubmitting ? (
<Trans i18nKey="admin:clients.buttons.creating" />
) : (
<Trans i18nKey="admin:clients.buttons.create" />
)}
</Button>
const { t } = useTranslation();
toast.success(t('admin:clients.toast.created'));
# Validate JSON syntax
node -e "JSON.parse(require('fs').readFileSync('apps/web/public/locales/en/admin.json', 'utf8'))"
pnpm build # Ensure no translation errors
// ❌ WRONG - Toast expects strings, not JSX
toast.success(<Trans i18nKey="admin:clients.toast.created" />);
// ✅ CORRECT
const { t } = useTranslation();
toast.success(t('admin:clients.toast.created'));
// ❌ WRONG
<Button>Cancel</Button>
<Button>Delete</Button>
// ✅ CORRECT - Use common namespace
<Button><Trans i18nKey="common:buttons.cancel" /></Button>
<Button><Trans i18nKey="common:buttons.delete" /></Button>
// ❌ WRONG - Duplicate "Cancel" across namespaces
// admin.json: { "buttons": { "cancel": "Cancel" } }
// user.json: { "buttons": { "cancel": "Cancel" } }
// ✅ CORRECT - Share common translations
<Trans i18nKey="common:buttons.cancel" />
// ❌ WRONG
<span className="sr-only">Open menu</span>
// ✅ CORRECT
<span className="sr-only">{t('admin:clients.actions.openMenu')}</span>
// ❌ WRONG - Defaults bypass translation system
<Trans i18nKey="admin:clients.title" defaults="Clients" />
// ✅ CORRECT - Add to JSON file, no defaults
<Trans i18nKey="admin:clients.title" />
// admin.json: { "clients": { "title": "Clients" } }
| Use Case | Pattern | Example |
|----------|---------|---------|
| UI Text | <Trans> | <Trans i18nKey="admin:clients.title" /> |
| Toast Messages | t() | toast.success(t('admin:clients.toast.created')) |
| Button States | Conditional <Trans> | {isLoading ? <Trans i18nKey="..." /> : <Trans i18nKey="..." />} |
| Table Headers | t() in column def | header: t('admin:clients.table.name') |
| Placeholders | t() in prop | placeholder={t('...')} |
| Accessibility | t() in sr-only | <span className="sr-only">{t('...')}</span> |
| Variables | values prop | <Trans i18nKey="..." values={{ name }} /> |
import { Trans } from '@kit/ui/trans';
import { withI18n } from '~/lib/i18n/with-i18n';
function HomePage() {
return (
<PrimaryHero
eyebrow={<Trans i18nKey="marketing:home.hero.eyebrow" />}
headline={<Trans i18nKey="marketing:home.hero.headline" />}
subheadline={<Trans i18nKey="marketing:home.hero.subheadline" />}
/>
);
}
export default withI18n(HomePage);
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 (
<Dialog>
<DialogContent>
<DialogHeader>
<DialogTitle>
<Trans i18nKey="admin:clients.createDialog.title" />
</DialogTitle>
<DialogDescription>
<Trans i18nKey="admin:clients.createDialog.description" />
</DialogDescription>
</DialogHeader>
<Form onSubmit={onSubmit}>
<FormField name="name">
<FormLabel><Trans i18nKey="admin:clients.form.name" /></FormLabel>
<Input placeholder="Fever" />
</FormField>
<DialogFooter>
<Button variant="outline">
<Trans i18nKey="common:buttons.cancel" />
</Button>
<Button type="submit">
{isSubmitting ? (
<Trans i18nKey="admin:clients.buttons.creating" />
) : (
<Trans i18nKey="admin:clients.buttons.create" />
)}
</Button>
</DialogFooter>
</Form>
</DialogContent>
</Dialog>
);
}
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 (
<div className="text-center py-8">
<h3><Trans i18nKey="admin:clients.empty.title" /></h3>
<p><Trans i18nKey="admin:clients.empty.description" /></p>
</div>
);
}
return (
<>
<TableSearchInput
placeholder={t('admin:clients.table.searchPlaceholder')}
/>
<DataTable columns={columns} data={clients} />
</>
);
}
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 }) => (
<DropdownMenu>
<DropdownMenuTrigger>
<span className="sr-only">{t('admin:clients.actions.openMenu')}</span>
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem onClick={() => onEdit(row.original)}>
{t('admin:clients.actions.edit')}
</DropdownMenuItem>
<DropdownMenuItem onClick={() => onDelete(row.original)}>
{t('admin:clients.actions.delete')}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
),
},
];
}
pnpm build # Ensure no missing translation errors
pnpm typecheck # Ensure types are correct
# Check for unused translation keys
grep -r "i18nKey" apps/web/app | grep -o '"[^"]*"' | sort | uniq
# Check for missing translations
# (manual comparison with JSON files)
Quick Start:
<Trans> for JSX, useTranslation() for stringscommon, admin, marketing, user)<Trans i18nKey="..." /> for UI componentst('...') for toast messages, placeholders, and string propsKey Learnings:
t(), not <Trans>t() function passed as parameter<Trans> componentscommon.jsonvalues propProduction Stats (as of 2025-11-23):
tools
# Test Patterns Testing patterns for reliable, maintainable, and fast tests. > **Template Usage:** Customize for your test framework (Vitest, Jest, Playwright, etc.) and assertion library. ## Test Structure ```typescript // user.test.ts import { describe, it, expect, beforeEach, afterEach } from 'vitest'; import { userService } from '@/services/user.service'; import { createTestUser, cleanupTestData } from '@/tests/helpers'; describe('UserService', () => { let testUserId: string; befor
tools
# State Management Patterns Client-side state management patterns for modern applications. > **Template Usage:** Customize for your state library (React Query, Zustand, Jotai, Redux, etc.). ## State Categories | Type | Description | Solution | |------|-------------|----------| | **Server State** | Data from API/database | React Query, SWR | | **Client State** | UI state, user preferences | Zustand, Jotai, useState | | **Form State** | Form inputs, validation | React Hook Form, Formik | | **U
development
# Service Patterns Service layer patterns for clean architecture with proper error handling, logging, and type safety. > **Template Usage:** Customize for your ORM (Prisma, Drizzle, TypeORM, etc.) and logging solution. ## Result Type Pattern Never throw exceptions from services. Always return a Result type. ```typescript // lib/result.ts export type Result<T, E = Error> = | { success: true; data: T } | { success: false; error: E }; export function ok<T>(data: T): Result<T, never> { r
testing
# Row-Level Security Patterns Database security patterns for multi-tenant and user-scoped data. > **Template Usage:** Customize for your database (PostgreSQL, Supabase, etc.) and auth system. ## RLS Fundamentals ### Enable RLS on Tables ```sql -- Enable RLS (required before policies take effect) ALTER TABLE users ENABLE ROW LEVEL SECURITY; ALTER TABLE posts ENABLE ROW LEVEL SECURITY; ALTER TABLE comments ENABLE ROW LEVEL SECURITY; -- Force RLS for table owners too (recommended) ALTER TABLE