I18n Date Patterns
Implements internationalization (i18n) in React applications. Covers user-facing strings, date/time handling, locale-aware formatting, ICU MessageFormat, and RTL support. Use when building multilingual UIs or formatting dates/currency.
Primary Agent: frontend-ui-developer
i18n and Localization Patterns
Overview
This skill provides comprehensive guidance for implementing internationalization in React applications. It ensures ALL user-facing strings, date displays, currency, lists, and time calculations are locale-aware.
When to use this skill:
- Adding ANY user-facing text to components
- Formatting dates, times, currency, lists, or ordinals
- Implementing complex pluralization
- Embedding React components in translated text
- Supporting RTL languages (Hebrew, Arabic)
Bundled Resources:
references/formatting-utilities.md- useFormatting hook API referencereferences/icu-messageformat.md- ICU plural/select syntaxreferences/trans-component.md- Trans component for rich textchecklists/i18n-checklist.md- Implementation and review checklistexamples/component-i18n-example.md- Complete component example
Canonical Reference: See docs/i18n-standards.md for the full i18n standards document.
Core Patterns
1. useTranslation Hook (All UI Strings)
Every visible string MUST use the translation function:
import { useTranslation } from 'react-i18next';
function MyComponent() {
const { t } = useTranslation(['patients', 'common']);
return (
<div>
<h1>{t('patients:title')}</h1>
<button>{t('common:actions.save')}</button>
</div>
);
}2. useFormatting Hook (Locale-Aware Data)
All locale-sensitive formatting MUST use the centralized hook:
import { useFormatting } from '@/hooks';
function PriceDisplay({ amount, items }) {
const { formatILS, formatList, formatOrdinal } = useFormatting();
return (
<div>
<p>Price: {formatILS(amount)}</p> {/* ₪1,500.00 */}
<p>Items: {formatList(items)}</p> {/* "a, b, and c" */}
<p>Position: {formatOrdinal(3)}</p> {/* "3rd" */}
</div>
);
}See references/formatting-utilities.md for the complete API.
3. Date Formatting
All dates MUST use the centralized @/lib/dates library:
import { formatDate, formatDateShort, calculateWaitTime } from '@/lib/dates';
const date = formatDate(appointment.date); // "Jan 6, 2026"
const waitTime = calculateWaitTime('09:30'); // "15 min"4. ICU MessageFormat (Complex Plurals)
Use ICU syntax in translation files for pluralization:
{
"patients": "{count, plural, =0 {No patients} one {# patient} other {# patients}}"
}t('patients', { count: 5 }) // → "5 patients"See references/icu-messageformat.md for full syntax.
5. Trans Component (Rich Text)
For embedded React components in translated text:
import { Trans } from 'react-i18next';
<Trans
i18nKey="richText.welcome"
values={{ name: userName }}
components={{ strong: <strong /> }}
/>See references/trans-component.md for patterns.
Translation File Structure
frontend/src/i18n/locales/
├── en/
│ ├── common.json # Shared: actions, status, time
│ ├── patients.json # Patient-related strings
│ ├── dashboard.json # Dashboard strings
│ ├── owner.json # Owner portal strings
│ └── invoices.json # Invoice strings
└── he/
└── (same structure)Anti-Patterns (FORBIDDEN)
// ❌ NEVER hardcode strings
<h1>מטופלים</h1> // Use t('patients:title')
<button>Save</button> // Use t('common:actions.save')
// ❌ NEVER use .join() for lists
items.join(', ') // Use formatList(items)
// ❌ NEVER hardcode currency
"₪" + price // Use formatILS(price)
// ❌ NEVER use new Date() for formatting
new Date().toLocaleDateString() // Use formatDate() from @/lib/dates
// ❌ NEVER use inline plural logic
count === 1 ? 'item' : 'items' // Use ICU MessageFormat
// ❌ NEVER leave console.log in production
console.log('debug') // Remove before commit
// ❌ NEVER use dangerouslySetInnerHTML for i18n
dangerouslySetInnerHTML // Use <Trans> componentQuick Reference
| Need | Solution |
|---|---|
| UI text | t('namespace:key') from useTranslation |
| Currency | formatILS(amount) from useFormatting |
| Lists | formatList(items) from useFormatting |
| Ordinals | formatOrdinal(n) from useFormatting |
| Dates | formatDate(date) from @/lib/dates |
| Plurals | ICU MessageFormat in translation files |
| Rich text | <Trans> component |
| RTL check | isRTL from useFormatting |
Checklist
See checklists/i18n-checklist.md for complete implementation and review checklists.
Integration with Agents
Frontend UI Developer
- Uses all i18n patterns for components
- References this skill for formatting
- Ensures no hardcoded strings
Code Quality Reviewer
- Checks for anti-patterns (
.join(),console.log, etc.) - Validates translation key coverage
- Ensures RTL compatibility
Skill Version: 1.2.0 Last Updated: 2026-01-06 Maintained by: Yonatan Gross
Related Skills
ork:testing-patterns- Comprehensive testing patterns including accessibility testing for i18ntype-safety-validation- Zod schemas for validating translation key structures and locale configsork:react-server-components-framework- Server-side locale detection and RSC i18n patternsork:accessibility- RTL-aware focus management for bidirectional UI navigation
Key Decisions
| Decision | Choice | Rationale |
|---|---|---|
| Translation Library | react-i18next | React-native hooks, namespace support, ICU format |
| Date Library | dayjs | Lightweight, locale plugins, immutable API |
| Message Format | ICU MessageFormat | Industry standard, complex plural/select support |
| Locale Storage | Per-namespace JSON | Code-splitting, lazy loading per feature |
| RTL Detection | CSS logical properties | Native browser support, no JS overhead |
Capability Details
translation-hooks
Keywords: useTranslation, t(), i18n hook, translation hook Solves:
- Translate UI strings with useTranslation
- Implement namespaced translations
- Handle missing translation keys
formatting-hooks
Keywords: useFormatting, formatCurrency, formatList, formatOrdinal Solves:
- Format currency values with locale
- Format lists with proper separators
- Handle ordinal numbers across locales
icu-messageformat
Keywords: ICU, MessageFormat, plural, select, pluralization Solves:
- Implement pluralization rules
- Handle gender-specific translations
- Build complex message patterns
date-time-formatting
Keywords: date format, time format, dayjs, locale date, calendar Solves:
- Format dates with dayjs and locale
- Handle timezone-aware formatting
- Build calendar components with i18n
rtl-support
Keywords: RTL, right-to-left, hebrew, arabic, direction Solves:
- Support RTL languages like Hebrew
- Handle bidirectional text
- Configure RTL-aware layouts
trans-component
Keywords: Trans, rich text, embedded JSX, interpolation Solves:
- Embed React components in translations
- Handle rich text formatting
- Implement safe HTML in translations
Rules (3)
Avoid hardcoded date and number formats that break in non-English locales — CRITICAL
i18n: Formatting Anti-Patterns
String concatenation, hardcoded currency symbols, manual list joining, and direct toLocaleString calls bypass the locale-aware formatting layer. These patterns silently break in RTL languages, produce incorrect currency symbols, and ignore locale-specific list conjunction rules.
Never concatenate or interpolate raw values into user-facing strings
Incorrect:
// String concatenation — breaks word order in RTL locales
const greeting = "Hello " + userName + "!";
// Template literal — same problem, locale-unaware
const message = `Welcome ${userName} to the dashboard`;
// Hardcoded currency symbol — wrong for non-ILS locales
<p>Price: ₪{price}</p>
<p>Total: ${price.toFixed(2)}</p>Correct:
import { useTranslation } from 'react-i18next';
import { useFormatting } from '@/hooks';
const { t } = useTranslation();
const { formatILS } = useFormatting();
// Translation key handles word order per locale
<p>{t('greeting', { name: userName })}</p>
// Locale-aware currency formatting
<p>{t('price_label')}: {formatILS(price)}</p>Never use .join() for user-facing lists
Incorrect:
const pets = ['Max', 'Bella', 'Charlie'];
// English-only comma joining — Hebrew uses "ו-" as conjunction
<p>Pets: {pets.join(', ')}</p>Correct:
const { formatList } = useFormatting();
// Produces "Max, Bella, and Charlie" (en) or "מקס, בלה ו-צ'רלי" (he)
<p>Pets: {formatList(pets)}</p>Never call toLocaleString directly
Incorrect:
// Hardcodes locale string, bypasses app-wide locale setting
const formatted = number.toLocaleString('he-IL');Correct:
const { formatNumber } = useFormatting();
// Automatically uses the app's current locale
const formatted = formatNumber(number);Key rules:
- Use
t('key', \{ variable \})with ICU MessageFormat placeholders instead of string concatenation or template literals - Use
useFormatting()hooks (formatILS,formatList,formatNumber,formatPercent) instead of hardcoded symbols or manual formatting - Never call
.join()on arrays for user-facing list display — useformatList()orformatListOr()which handle locale-specific conjunctions - Never call
toLocaleString()directly — it bypasses the app's locale management and cannot react to language changes
Reference: references/formatting-utilities.md (lines 132-167)
Use ICU plural rules to handle complex plural forms across all locales correctly — HIGH
i18n: ICU Plural Rules
ICU MessageFormat provides locale-aware pluralization via \{variable, plural, ...\} syntax in translation files. Hardcoding plural logic in JavaScript with ternaries or conditionals only works for English (one/other) and breaks for languages with more plural categories — Hebrew has a dual form, Arabic has six forms (zero, one, two, few, many, other).
Never use conditional logic in code for plurals
Incorrect:
// Ternary pluralization — only handles English
const message = count === 0
? 'No items'
: count === 1
? '1 item'
: `${count} items`;
// Conditional with template literal — same problem
const label = `${count} patient${count !== 1 ? 's' : ''}`;Correct:
// Translation file (en.json):
// "items": "{count, plural, =0 {No items} one {# item} other {# items}}"
// "patients": "{count, plural, =0 {No patients} one {# patient} other {# patients}}"
import { useTranslation } from 'react-i18next';
function PatientCount({ count }) {
const { t } = useTranslation();
// ICU handles plural category selection per locale
return <span>{t('patients', { count })}</span>;
}Always include the other category
Every ICU plural message MUST include the other case. It is the mandatory fallback category used by all locales. Omitting it causes runtime errors or blank output for unmatched counts.
Incorrect:
{
"items": "{count, plural, =0 {None} one {One item}}"
}Correct:
{
"items": "{count, plural, =0 {None} one {One item} other {# items}}"
}Handle locale-specific plural categories and =0 for zero states
Hebrew uses a two (dual) form. Arabic uses zero, one, two, few, many, and other. The =0 exact-match takes priority over the zero category and works across all locales.
// Hebrew (he.json) — includes dual form:
{ "items": "{count, plural, =0 {אין פריטים} one {פריט #} two {# פריטים} other {# פריטים}}" }
// English — use =0 for empty states:
{ "appointments": "{count, plural, =0 {No upcoming appointments} one {# appointment} other {# appointments}}" }Key rules:
- Never use ternaries, conditionals, or template literals for pluralization — always use ICU
\{count, plural, ...\}in translation files - Every
pluralmessage must include theothercategory as a mandatory fallback - Provide locale-specific categories (
twofor Hebrew,few/manyfor Arabic, Slavic languages) in the respective translation files - Use
=0exact match for zero/empty states instead of relying on thezeroplural category
Reference: references/icu-messageformat.md (lines 13-28, 141-165)
Use the Trans component for JSX-embedded translations that preserve locale word order — HIGH
i18n: Trans Component
The <Trans> component from react-i18next embeds React elements (links, bold, icons) inside translated strings. Without it, developers split translations around JSX — breaking word order in other locales — or resort to dangerouslySetInnerHTML, which introduces XSS vulnerabilities.
Never concatenate translated strings with JSX between them
Incorrect:
// Splitting translation around JSX — word order breaks in RTL/other locales
<p>{t('welcome')} <strong>{userName}</strong> {t('toDashboard')}</p>Correct:
import { Trans } from 'react-i18next';
// Translation: "Welcome <strong>{{name}}</strong> to the dashboard!"
<Trans
i18nKey="welcomeUser"
values={{ name: userName }}
components={{ strong: <strong className="font-bold" /> }}
/>Never use dangerouslySetInnerHTML for rich translated text
Incorrect:
<p dangerouslySetInnerHTML={{ __html: t('richContent') }} /> // XSS risk!Correct:
<Trans i18nKey="richContent" components={{ bold: <strong />, link: <a href="/help" /> }} />Prefer named components over indexed tags
Incorrect:
// Indexed tags — fragile, order-dependent
// Translation: "Click <0>here</0> to <1>learn more</1>."
<Trans i18nKey="simple" components={[
<a href="/action" />, <span className="font-bold" />
]} />Correct:
// Named tags — self-documenting, order-independent
// Translation: "Click <link>here</link> to <bold>learn more</bold>."
<Trans i18nKey="simple" components={{
link: <a href="/action" />,
bold: <span className="font-bold" />
}} />Key rules:
- Never split a sentence across multiple
t()calls with JSX between them — use a single<Trans>withcomponentsmapping - Never use
dangerouslySetInnerHTMLfor rich translated text —<Trans>provides safe component interpolation - Prefer named component tags (
<link>,<bold>) over indexed tags (<0>,<1>) in translation strings - Use
t()for plain text and<Trans>only when JSX elements must appear inside the translated string
Reference: references/trans-component.md (lines 23-38, 207-235)
References (3)
Formatting Utilities
Formatting Utilities Reference
Overview
This reference documents the useFormatting hook and related formatting utilities for locale-aware data display in the application React components.
Primary Source: frontend/src/hooks/useFormatting.ts
Implementation: frontend/src/lib/formatting.ts
Standards Doc: docs/i18n-standards.md
useFormatting Hook
The useFormatting hook provides locale-aware formatting functions that automatically re-render when the language changes.
Basic Usage
import { useFormatting } from '@/hooks';
function MyComponent() {
const {
formatILS,
formatList,
formatListOr,
formatOrdinal,
formatDuration,
formatRelativeTime,
formatPercent,
formatWeight,
isRTL,
locale
} = useFormatting();
return (
<div dir={isRTL ? 'rtl' : 'ltr'}>
<p>Price: {formatILS(1500)}</p>
<p>Pets: {formatList(['Max', 'Bella', 'Charlie'])}</p>
<p>Position: {formatOrdinal(3)}</p>
</div>
);
}Available Formatters
Currency Formatting
| Function | Purpose | Hebrew Output | English Output |
|---|---|---|---|
formatILS(amount) | Israeli Shekel with locale | ₪1,234.56 | $1,234.56 |
formatCurrency(amount, code) | Any currency | Varies | Varies |
formatILS(1500) // → "₪1,500.00" (he) / "$1,500.00" (en)
formatCurrency(99.99, 'EUR') // → "€99.99"Number Formatting
| Function | Purpose | Example |
|---|---|---|
formatNumber(n) | Locale-aware number | 1,234.56 |
formatPercent(n) | Percentage | 85% |
formatCompact(n) | Compact notation | 1.5K |
formatWeight(n) | Weight with units | 5.5 kg / 5.5 ק"ג |
formatDecimal(n, places) | Fixed decimal places | 3.14 |
formatPercent(0.85) // → "85%"
formatCompact(1500) // → "1.5K"
formatWeight(5.5) // → "5.5 kg" (en) / '5.5 ק"ג' (he)List Formatting
| Function | Purpose | Hebrew Output | English Output |
|---|---|---|---|
formatList(items) | "and" conjunction | א, ב ו-ג | a, b, and c |
formatListOr(items) | "or" conjunction | א, ב או ג | a, b, or c |
formatListUnits(items) | Unit list | א, ב, ג | a, b, c |
formatList(['Max', 'Bella', 'Charlie'])
// → "Max, Bella, and Charlie" (en)
// → "מקס, בלה ו-צ'רלי" (he)
formatListOr(['dog', 'cat'])
// → "dog or cat" (en)
// → "כלב או חתול" (he)Time Formatting
| Function | Purpose | Example |
|---|---|---|
formatRelativeTime(date) | Time ago/until | 2 days ago |
formatTimeUntil(date) | Time until future | in 3 hours |
formatTimeSince(date) | Time since past | 5 minutes ago |
formatDuration(seconds) | Human-readable duration | 1 hr 30 min |
formatDurationClock(seconds) | Clock format | 01:30:00 |
formatRelativeTime(yesterday) // → "yesterday" / "אתמול"
formatDuration(3661) // → "1 hr 1 min 1 sec"Ordinal Formatting
| Function | Purpose | Hebrew Output | English Output |
|---|---|---|---|
formatOrdinal(n) | Ordinal number | 3. | 3rd |
formatPosition(n) | Position label | מקום 3 | 3rd place |
formatOrdinal(1) // → "1st" (en) / "1." (he)
formatOrdinal(3) // → "3rd" (en) / "3." (he)
formatOrdinal(22) // → "22nd" (en) / "22." (he)Date Range Formatting
| Function | Purpose | Example |
|---|---|---|
formatDateRange(start, end) | Date range | Jan 5 – 10, 2026 |
Anti-Patterns
❌ NEVER use .join() for user-facing lists
// ❌ WRONG
const pets = ['Max', 'Bella', 'Charlie'];
<p>Pets: {pets.join(', ')}</p>
// ✅ CORRECT
const { formatList } = useFormatting();
<p>Pets: {formatList(pets)}</p>❌ NEVER hardcode currency symbols
// ❌ WRONG
<p>Price: ₪{price}</p>
<p>Price: ${price.toFixed(2)}</p>
// ✅ CORRECT
const { formatILS } = useFormatting();
<p>Price: {formatILS(price)}</p>❌ NEVER use toLocaleString directly
// ❌ WRONG
const formatted = number.toLocaleString('he-IL');
// ✅ CORRECT
const { formatNumber } = useFormatting();
const formatted = formatNumber(number);Integration with useTranslation
The useFormatting hook complements useTranslation:
import { useTranslation } from 'react-i18next';
import { useFormatting } from '@/hooks';
function InvoiceSummary({ total, items }) {
const { t } = useTranslation('invoices');
const { formatILS, formatList } = useFormatting();
return (
<div>
<h2>{t('summary.title')}</h2>
<p>{t('summary.total')}: {formatILS(total)}</p>
<p>{t('summary.items')}: {formatList(items.map(i => i.name))}</p>
</div>
);
}Locale Properties
const { locale, isRTL } = useFormatting();
// locale: 'he-IL' | 'en-US'
// isRTL: true (Hebrew) | false (English)Last Updated: 2026-01-06
Icu Messageformat
ICU MessageFormat Reference
Overview
ICU MessageFormat provides advanced pluralization and selection logic in translation files. the application uses i18next-icu for ICU support.
Dependencies:
i18next-icu v2.4.1- ICU MessageFormat syntax
Plural Rules
Basic Plural
{
"patients": "{count, plural, =0 {No patients} one {# patient} other {# patients}}"
}Usage:
t('patients', { count: 0 }) // → "No patients"
t('patients', { count: 1 }) // → "1 patient"
t('patients', { count: 5 }) // → "5 patients"Hebrew Plural (with dual form)
Hebrew has special forms for dual (two) numbers:
{
"items": "{count, plural, =0 {אין פריטים} one {פריט #} two {# פריטים} other {# פריטים}}"
}Offset Plurals
{
"guests": "{count, plural, offset:1 =0 {No guests} =1 {One guest} one {# guests and one other} other {# guests and # others}}"
}Select Rules
For non-numeric selection (gender, type, etc.):
{
"petGreeting": "{species, select, dog {Good boy!} cat {Nice kitty!} other {Hello pet!}}"
}Usage:
t('petGreeting', { species: 'dog' }) // → "Good boy!"
t('petGreeting', { species: 'cat' }) // → "Nice kitty!"
t('petGreeting', { species: 'bird' }) // → "Hello pet!"Ordinal Rules
{
"position": "{position, selectordinal, one {#st} two {#nd} few {#rd} other {#th}}"
}Usage:
t('position', { position: 1 }) // → "1st"
t('position', { position: 2 }) // → "2nd"
t('position', { position: 3 }) // → "3rd"
t('position', { position: 4 }) // → "4th"Number Formatting in ICU
{
"price": "The price is {amount, number, ::currency/ILS}"
}Date Formatting in ICU
{
"appointment": "Your appointment is on {date, date, medium}"
}Nested Messages
Combine plural and select:
{
"petStatus": "{species, select, dog {{count, plural, =0 {No dogs} one {# dog} other {# dogs}}} cat {{count, plural, =0 {No cats} one {# cat} other {# cats}}} other {{count, plural, =0 {No pets} one {# pet} other {# pets}}}}"
}Common Patterns for the application
Counts with Zero State
{
"vaccinations": "{count, plural, =0 {No vaccinations recorded} one {# vaccination} other {# vaccinations}}",
"medications": "{count, plural, =0 {No active medications} one {# medication} other {# medications}}",
"appointments": "{count, plural, =0 {No upcoming appointments} one {# appointment} other {# appointments}}"
}Time Remaining
{
"daysRemaining": "{count, plural, =0 {Due today} one {# day remaining} other {# days remaining}}"
}Anti-Patterns
❌ NEVER use conditional logic in code for plurals
// ❌ WRONG
const message = count === 0 ? 'No items' : count === 1 ? '1 item' : `${count} items`;
// ✅ CORRECT
const message = t('items', { count });❌ NEVER forget the "other" case
// ❌ WRONG - missing "other"
{
"items": "{count, plural, =0 {None} one {One}}"
}
// ✅ CORRECT
{
"items": "{count, plural, =0 {None} one {One} other {#}}"
}Last Updated: 2026-01-06
Trans Component
Trans Component Reference
Overview
The <Trans> component from react-i18next enables embedding React components within translated text. Use this for rich text formatting, links, and interactive elements.
Basic Usage
Translation File
{
"richText": {
"welcome": "Welcome <strong>{{name}}</strong> to the application!",
"terms": "By continuing, you agree to our <link>Terms of Service</link>.",
"highlight": "Your pet <bold>{{petName}}</bold> has <count>{{count}}</count> upcoming appointments."
}
}Component Usage
import { Trans } from 'react-i18next';
function WelcomeMessage({ userName }) {
return (
<Trans
i18nKey="richText.welcome"
values={{ name: userName }}
components={{ strong: <strong className="font-bold" /> }}
/>
);
}
// Output: Welcome <strong>John</strong> to the application!Component Mapping
Named Components
<Trans
i18nKey="richText.terms"
components={{
link: <a href="/terms" className="text-primary underline" />
}}
/>Multiple Components
<Trans
i18nKey="richText.highlight"
values={{ petName: 'Max', count: 3 }}
components={{
bold: <strong className="font-semibold" />,
count: <span className="text-primary font-bold" />
}}
/>Self-Closing Tags
For components without children (icons, line breaks):
Translation File
{
"withIcon": "Click here <icon/> to continue",
"multiLine": "Line one<br/>Line two"
}Component Usage
import { AlertCircle } from 'lucide-react';
<Trans
i18nKey="withIcon"
components={{
icon: <AlertCircle className="inline h-4 w-4" />
}}
/>
<Trans
i18nKey="multiLine"
components={{
br: <br />
}}
/>Indexed Components
For simpler cases, use indexed tags:
Translation File
{
"simple": "Click <0>here</0> to <1>learn more</1>."
}Component Usage
<Trans
i18nKey="simple"
components={[
<a href="/action" className="text-primary" />,
<span className="font-bold" />
]}
/>With Interpolation
Combine values and components:
<Trans
i18nKey="message"
values={{
name: user.name,
date: formattedDate
}}
components={{
bold: <strong />,
link: <Link to="/profile" />
}}
/>Pluralization with Trans
{
"petCount": "{count, plural, =0 {You have <bold>no pets</bold>} one {You have <bold># pet</bold>} other {You have <bold># pets</bold>}}"
}<Trans
i18nKey="petCount"
values={{ count: petCount }}
components={{ bold: <strong /> }}
/>Common Patterns for the application
Highlighted Pet Names
{
"visitSummary": "Visit summary for <petName>{{name}}</petName>"
}<Trans
i18nKey="visitSummary"
values={{ name: pet.name }}
components={{
petName: <span className="font-semibold text-primary" />
}}
/>Action Links
{
"addPet": "Don't see your pet? <addLink>Add a new pet</addLink>"
}<Trans
i18nKey="addPet"
components={{
addLink: <button onClick={onAddPet} className="text-primary underline" />
}}
/>Anti-Patterns
❌ NEVER concatenate translated strings with JSX
// ❌ WRONG
<p>
{t('welcome')} <strong>{userName}</strong> {t('tothe application')}
</p>
// ✅ CORRECT
<Trans
i18nKey="welcomeUser"
values={{ name: userName }}
components={{ strong: <strong /> }}
/>❌ NEVER use dangerouslySetInnerHTML for rich text
// ❌ WRONG (security risk!)
<p dangerouslySetInnerHTML={{ __html: t('richContent') }} />
// ✅ CORRECT
<Trans i18nKey="richContent" components={{ ... }} />TypeScript Support
import { Trans, TransProps } from 'react-i18next';
// Type-safe component props
const transProps: TransProps<string> = {
i18nKey: 'myKey',
values: { name: 'John' },
components: { bold: <strong /> }
};Last Updated: 2026-01-06
Checklists (1)
I18n Checklist
i18n Implementation Checklist
Use this checklist when adding or reviewing i18n in the application components.
New Component Checklist
UI Strings
- Import
useTranslationfromreact-i18next - All visible text uses
t('namespace:key')function - No hardcoded Hebrew strings (e.g.,
מטופלים) - No hardcoded English strings (e.g.,
Save,Cancel) - Translation keys added to both
en/*.jsonandhe/*.json - Key naming follows convention:
category.subcategory.action
Formatting
- Import
useFormattingfrom@/hooksfor locale-aware data - Currency uses
formatILS()not₪$\{price\} - Lists use
formatList()not.join(', ') - Ordinals use
formatOrdinal()not hardcoded suffixes - Percentages use
formatPercent()not$\{n\}%
Dates & Times
- Import from
@/lib/dates, notdayjsdirectly - No
new Date().toLocaleDateString() - No hardcoded date formats (e.g.,
DD/MM/YYYY) - Use appropriate helper:
formatDate,formatDateShort,formatFullDate - Wait times use
calculateWaitTime()
Pluralization
- Complex plurals use ICU MessageFormat in translation files
- No conditional ternary logic for plural forms in code
- Hebrew dual forms (two) handled when applicable
- All plural keys include
othercase
Rich Text
- Embedded components use
<Trans>component - No string concatenation with JSX
- No
dangerouslySetInnerHTMLfor translated content
RTL Support
- Component respects
isRTLfor directional styling - Text alignment adapts to locale
- Icons/arrows flip appropriately in RTL
Code Review Checklist
Forbidden Patterns
- ❌ No
.join(', ')for user-facing lists - ❌ No
console.logstatements in production code - ❌ No hardcoded currency symbols (
₪,$) - ❌ No
new Date()for formatting - ❌ No inline locale strings (
דקות,minutes) - ❌ No conditional pluralization in code
Required Patterns
- ✅
useTranslationhook present - ✅
useFormattinghook for locale-sensitive data - ✅ All translation keys exist in both locales
- ✅ Component tested with language switch
Migration Checklist (Existing Component)
When updating a component to use proper i18n:
- Identify all hardcoded strings
- Create translation keys in appropriate namespace
- Add translations to
en/*.jsonandhe/*.json - Replace hardcoded strings with
t()calls - Replace
.join()withformatList() - Replace date formatting with
@/lib/dateshelpers - Replace currency with
formatILS() - Remove any
console.logstatements - Test language switching
- Test RTL layout (if applicable)
Quality Metrics
| Metric | Target | How to Check |
|---|---|---|
Components with useTranslation | 100% | grep -r "useTranslation" --include="*.tsx" |
Components with useFormatting | 80%+ | grep -r "useFormatting" --include="*.tsx" |
| Console.log statements | 0 | grep -r "console.log" --include="*.tsx" |
Hardcoded .join() | 0 | grep -r "\.join(" --include="*.tsx" |
Raw dayjs().format() | 0 | grep -r "dayjs().format" --include="*.tsx" |
Last Updated: 2026-01-06
Examples (1)
Component I18n Example
Component i18n Example
Complete Example: Invoice Summary Component
This example demonstrates all i18n patterns in a single component.
Before (Anti-Patterns)
// ❌ WRONG: Multiple i18n anti-patterns
function InvoiceSummary({ invoice }) {
const items = invoice.items.map(i => i.name);
const dueDate = new Date(invoice.dueDate);
console.log('Rendering invoice:', invoice.id); // ❌ console.log
return (
<div>
<h2>Invoice Summary</h2> {/* ❌ Hardcoded string */}
<p>Total: ₪{invoice.total.toFixed(2)}</p> {/* ❌ Hardcoded currency */}
<p>Items: {items.join(', ')}</p> {/* ❌ .join() for list */}
<p>Due: {dueDate.toLocaleDateString('he-IL')}</p> {/* ❌ Raw Date */}
<p>
{invoice.itemCount === 1 ? '1 item' : `${invoice.itemCount} items`} {/* ❌ Inline plural */}
</p>
<p>Position: {invoice.priority}st</p> {/* ❌ Hardcoded ordinal */}
</div>
);
}After (Correct Patterns)
// ✅ CORRECT: All i18n patterns properly implemented
import { useTranslation, Trans } from 'react-i18next';
import { useFormatting } from '@/hooks';
import { formatDate } from '@/lib/dates';
function InvoiceSummary({ invoice }) {
const { t } = useTranslation('invoices');
const { formatILS, formatList, formatOrdinal } = useFormatting();
const itemNames = invoice.items.map(i => i.name);
return (
<div>
<h2>{t('summary.title')}</h2>
{/* Currency formatting */}
<p>{t('summary.total')}: {formatILS(invoice.total)}</p>
{/* List formatting */}
<p>{t('summary.items')}: {formatList(itemNames)}</p>
{/* Date formatting */}
<p>{t('summary.dueDate')}: {formatDate(invoice.dueDate)}</p>
{/* ICU plural (in translation file) */}
<p>{t('summary.itemCount', { count: invoice.itemCount })}</p>
{/* Ordinal formatting */}
<p>{t('summary.priority')}: {formatOrdinal(invoice.priority)}</p>
{/* Rich text with Trans */}
<Trans
i18nKey="invoices:summary.paymentNote"
values={{ amount: formatILS(invoice.total) }}
components={{ bold: <strong className="font-semibold" /> }}
/>
</div>
);
}Translation Files
en/invoices.json:
{
"summary": {
"title": "Invoice Summary",
"total": "Total",
"items": "Items",
"dueDate": "Due Date",
"itemCount": "{count, plural, =0 {No items} one {# item} other {# items}}",
"priority": "Priority",
"paymentNote": "Please pay <bold>{{amount}}</bold> by the due date."
}
}he/invoices.json:
{
"summary": {
"title": "סיכום חשבונית",
"total": "סה״כ",
"items": "פריטים",
"dueDate": "תאריך יעד",
"itemCount": "{count, plural, =0 {אין פריטים} one {פריט #} two {# פריטים} other {# פריטים}}",
"priority": "עדיפות",
"paymentNote": "אנא שלם <bold>{{amount}}</bold> עד תאריך היעד."
}
}Pattern Summary
| Pattern | Wrong | Correct |
|---|---|---|
| Strings | "Invoice" | t('invoices:title') |
| Currency | ₪$\{total\} | formatILS(total) |
| Lists | items.join(', ') | formatList(items) |
| Dates | date.toLocaleDateString() | formatDate(date) |
| Plurals | count === 1 ? 'item' : 'items' | t('key', \{ count \}) |
| Ordinals | $\{n\}st | formatOrdinal(n) |
| Rich text | String concat with JSX | <Trans> component |
| Debug | console.log() | Remove before commit |
Last Updated: 2026-01-06
Help
OrchestKit skill directory with categorized listings. Use when discovering skills for a task, finding the right workflow, or browsing capabilities.
Implement
Full-power feature implementation with parallel subagents. Use when implementing, building, or creating features.
Last updated on