6. Internationalization (i18n) Implementation
Date: 2025-12-23
Status
Accepted
Context
Users need to view the UI in English and Spanish, with different copy for each state (DC, CO). Content is managed in a Google Sheet that is updated regularly and downloaded to the repository periodically.
Decision
Use react-i18next with i18next for internationalization.
Why react-i18next over next-intl
| Factor | next-intl |
react-i18next |
|---|---|---|
| Ecosystem | Next.js only | Universal (React Native, Electron, etc.) |
| Maturity | ~3 years | 10+ years, battle-tested |
| Plugins | Limited | Rich ecosystem (ICU, backend loaders, etc.) |
| Interpolation | Basic | Full ICU MessageFormat (plurals, gender, etc.) |
| SSR support | Built-in | Supported with config |
| Community | Growing | Massive, well-documented |
| Namespace loading | All-or-nothing | Lazy load per page/component |
Reference: https://react.i18next.com/guides/the-drawbacks-of-other-i18n-solutions
Architecture
Google Sheet (source of truth)
↓ (manual download as CSV)
content/
├── copy.csv # Downloaded sheet (committed)
└── locales/ # Generated at build time (gitignored)
├── en/
│ ├── dc/
│ │ ├── common.json # Shared strings (header, footer)
│ │ └── landing.json # Page-specific
│ └── co/
└── es/
├── dc/
└── co/
Key Structure
CSV row:
Content,English,Español
S1 - Landing Page - Title,Get a one-time payment...,Obtenga un pago único...
Generated JSON (content/locales/en/dc/landing.json):
{
"title": "Get a one-time payment of $120 in summer food benefits for your child",
"body": "Each eligible child can receive a *one-time payment of $120*...",
"action": "Do I need to apply?"
}
Commands
pnpm copy:generate # CSV → JSON per locale/state/namespace
pnpm copy:validate # Ensure all keys exist across locales
pnpm copy:extract # Extract keys from code (i18next-parser)
Runtime Usage
import { useTranslation } from 'react-i18next'
function LandingPage() {
const { t } = useTranslation('landing')
return (
<>
<h1>{t('title')}</h1>
<p>{t('body')}</p>
<button>{t('action')}</button>
</>
)
}
Implementation Phases
- Phase 1: Generation script (CSV → namespaced JSON)
- Phase 2: Install
react-i18next,i18next, configure - Phase 3: Add
I18nextProviderto layout - Phase 4: Replace hardcoded strings with
t()calls - Phase 5: Wire language buttons to
i18n.changeLanguage()
Consequences
Positive
- Battle-tested solution with extensive documentation
- Flexible namespace loading for performance
- ICU MessageFormat support for complex pluralization
- Easy to add new languages/states
- Content team can update copy without code changes
Negative
- Requires build step to generate JSON from CSV
- Additional client-side JavaScript bundle
- Need to maintain key consistency between sheet and code
Neutral
- Language persisted via cookie/localStorage
- State determined at build time (existing pattern)