Skip to main content
OrchestKit v6.7.1 — 67 skills, 38 agents, 77 hooks with Opus 4.6 support
OrchestKit
Skills

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.

Reference low

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 reference
  • references/icu-messageformat.md - ICU plural/select syntax
  • references/trans-component.md - Trans component for rich text
  • checklists/i18n-checklist.md - Implementation and review checklist
  • examples/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> component

Quick Reference

NeedSolution
UI textt('namespace:key') from useTranslation
CurrencyformatILS(amount) from useFormatting
ListsformatList(items) from useFormatting
OrdinalsformatOrdinal(n) from useFormatting
DatesformatDate(date) from @/lib/dates
PluralsICU MessageFormat in translation files
Rich text&lt;Trans&gt; component
RTL checkisRTL 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

  • ork:testing-patterns - Comprehensive testing patterns including accessibility testing for i18n
  • type-safety-validation - Zod schemas for validating translation key structures and locale configs
  • ork:react-server-components-framework - Server-side locale detection and RSC i18n patterns
  • ork:accessibility - RTL-aware focus management for bidirectional UI navigation

Key Decisions

DecisionChoiceRationale
Translation Libraryreact-i18nextReact-native hooks, namespace support, ICU format
Date LibrarydayjsLightweight, locale plugins, immutable API
Message FormatICU MessageFormatIndustry standard, complex plural/select support
Locale StoragePer-namespace JSONCode-splitting, lazy loading per feature
RTL DetectionCSS logical propertiesNative 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 — use formatList() or formatListOr() 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 plural message must include the other category as a mandatory fallback
  • Provide locale-specific categories (two for Hebrew, few/many for Arabic, Slavic languages) in the respective translation files
  • Use =0 exact match for zero/empty states instead of relying on the zero plural 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 &lt;Trans&gt; 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 &lt;Trans&gt; with components mapping
  • Never use dangerouslySetInnerHTML for rich translated text — &lt;Trans&gt; provides safe component interpolation
  • Prefer named component tags (&lt;link&gt;, &lt;bold&gt;) over indexed tags (&lt;0&gt;, &lt;1&gt;) in translation strings
  • Use t() for plain text and &lt;Trans&gt; 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

FunctionPurposeHebrew OutputEnglish Output
formatILS(amount)Israeli Shekel with locale₪1,234.56$1,234.56
formatCurrency(amount, code)Any currencyVariesVaries
formatILS(1500)      // → "₪1,500.00" (he) / "$1,500.00" (en)
formatCurrency(99.99, 'EUR') // → "€99.99"

Number Formatting

FunctionPurposeExample
formatNumber(n)Locale-aware number1,234.56
formatPercent(n)Percentage85%
formatCompact(n)Compact notation1.5K
formatWeight(n)Weight with units5.5 kg / 5.5 ק"ג
formatDecimal(n, places)Fixed decimal places3.14
formatPercent(0.85)   // → "85%"
formatCompact(1500)   // → "1.5K"
formatWeight(5.5)     // → "5.5 kg" (en) / '5.5 ק"ג' (he)

List Formatting

FunctionPurposeHebrew OutputEnglish 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

FunctionPurposeExample
formatRelativeTime(date)Time ago/until2 days ago
formatTimeUntil(date)Time until futurein 3 hours
formatTimeSince(date)Time since past5 minutes ago
formatDuration(seconds)Human-readable duration1 hr 30 min
formatDurationClock(seconds)Clock format01:30:00
formatRelativeTime(yesterday)  // → "yesterday" / "אתמול"
formatDuration(3661)           // → "1 hr 1 min 1 sec"

Ordinal Formatting

FunctionPurposeHebrew OutputEnglish Output
formatOrdinal(n)Ordinal number3.3rd
formatPosition(n)Position labelמקום 33rd place
formatOrdinal(1)   // → "1st" (en) / "1." (he)
formatOrdinal(3)   // → "3rd" (en) / "3." (he)
formatOrdinal(22)  // → "22nd" (en) / "22." (he)

Date Range Formatting

FunctionPurposeExample
formatDateRange(start, end)Date rangeJan 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 &lt;Trans&gt; 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" />
  }}
/>
{
  "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 useTranslation from react-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/*.json and he/*.json
  • Key naming follows convention: category.subcategory.action

Formatting

  • Import useFormatting from @/hooks for 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, not dayjs directly
  • 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 other case

Rich Text

  • Embedded components use &lt;Trans&gt; component
  • No string concatenation with JSX
  • No dangerouslySetInnerHTML for translated content

RTL Support

  • Component respects isRTL for 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.log statements in production code
  • ❌ No hardcoded currency symbols (, $)
  • ❌ No new Date() for formatting
  • ❌ No inline locale strings (דקות, minutes)
  • ❌ No conditional pluralization in code

Required Patterns

  • useTranslation hook present
  • useFormatting hook 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:

  1. Identify all hardcoded strings
  2. Create translation keys in appropriate namespace
  3. Add translations to en/*.json and he/*.json
  4. Replace hardcoded strings with t() calls
  5. Replace .join() with formatList()
  6. Replace date formatting with @/lib/dates helpers
  7. Replace currency with formatILS()
  8. Remove any console.log statements
  9. Test language switching
  10. Test RTL layout (if applicable)

Quality Metrics

MetricTargetHow to Check
Components with useTranslation100%grep -r "useTranslation" --include="*.tsx"
Components with useFormatting80%+grep -r "useFormatting" --include="*.tsx"
Console.log statements0grep -r "console.log" --include="*.tsx"
Hardcoded .join()0grep -r "\.join(" --include="*.tsx"
Raw dayjs().format()0grep -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

PatternWrongCorrect
Strings"Invoice"t('invoices:title')
Currency₪$\{total\}formatILS(total)
Listsitems.join(', ')formatList(items)
Datesdate.toLocaleDateString()formatDate(date)
Pluralscount === 1 ? 'item' : 'items't('key', \{ count \})
Ordinals$\{n\}stformatOrdinal(n)
Rich textString concat with JSX&lt;Trans&gt; component
Debugconsole.log()Remove before commit

Last Updated: 2026-01-06

Edit on GitHub

Last updated on

On this page

i18n and Localization PatternsOverviewCore Patterns1. useTranslation Hook (All UI Strings)2. useFormatting Hook (Locale-Aware Data)3. Date Formatting4. ICU MessageFormat (Complex Plurals)5. Trans Component (Rich Text)Translation File StructureAnti-Patterns (FORBIDDEN)Quick ReferenceChecklistIntegration with AgentsFrontend UI DeveloperCode Quality ReviewerRelated SkillsKey DecisionsCapability Detailstranslation-hooksformatting-hooksicu-messageformatdate-time-formattingrtl-supporttrans-componentRules (3)Avoid hardcoded date and number formats that break in non-English locales — CRITICALi18n: Formatting Anti-PatternsNever concatenate or interpolate raw values into user-facing stringsNever use .join() for user-facing listsNever call toLocaleString directlyUse ICU plural rules to handle complex plural forms across all locales correctly — HIGHi18n: ICU Plural RulesNever use conditional logic in code for pluralsAlways include the other categoryHandle locale-specific plural categories and =0 for zero statesUse the Trans component for JSX-embedded translations that preserve locale word order — HIGHi18n: Trans ComponentNever concatenate translated strings with JSX between themNever use dangerouslySetInnerHTML for rich translated textPrefer named components over indexed tagsReferences (3)Formatting UtilitiesFormatting Utilities ReferenceOverviewuseFormatting HookBasic UsageAvailable FormattersCurrency FormattingNumber FormattingList FormattingTime FormattingOrdinal FormattingDate Range FormattingAnti-Patterns❌ NEVER use .join() for user-facing lists❌ NEVER hardcode currency symbols❌ NEVER use toLocaleString directlyIntegration with useTranslationLocale PropertiesIcu MessageformatICU MessageFormat ReferenceOverviewPlural RulesBasic PluralHebrew Plural (with dual form)Offset PluralsSelect RulesOrdinal RulesNumber Formatting in ICUDate Formatting in ICUNested MessagesCommon Patterns for the applicationCounts with Zero StateTime RemainingAnti-Patterns❌ NEVER use conditional logic in code for plurals❌ NEVER forget the "other" caseTrans ComponentTrans Component ReferenceOverviewBasic UsageTranslation FileComponent UsageComponent MappingNamed ComponentsMultiple ComponentsSelf-Closing TagsTranslation FileComponent UsageIndexed ComponentsTranslation FileComponent UsageWith InterpolationPluralization with TransCommon Patterns for the applicationHighlighted Pet NamesAction LinksAnti-Patterns❌ NEVER concatenate translated strings with JSX❌ NEVER use dangerouslySetInnerHTML for rich textTypeScript SupportChecklists (1)I18n Checklisti18n Implementation ChecklistNew Component ChecklistUI StringsFormattingDates & TimesPluralizationRich TextRTL SupportCode Review ChecklistForbidden PatternsRequired PatternsMigration Checklist (Existing Component)Quality MetricsExamples (1)Component I18n ExampleComponent i18n ExampleComplete Example: Invoice Summary ComponentBefore (Anti-Patterns)After (Correct Patterns)Translation FilesPattern Summary