How do you handle internationalization (i18n) and accessibility (a11y) in React apps?
7 minintermediatereactbest-practiceshandleinternationalizationi18naccessibility
Quick Answer
Key aspects: Internationalization (i18n) with react-i18next; Using i18n in Components; Accessibility (a11y) Implementation; Accessible Navigation; Accessible Data Tables; Custom Accessibility Hooks.
Detailed Answer
How do you handle internationalization (i18n) and accessibility (a11y) in React apps?
Answer:
1. Internationalization (i18n) with react-i18next:
// i18n configuration
import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
import LanguageDetector from 'i18next-browser-languagedetector';
// Translation resources
const resources = {
en: {
translation: {
welcome: 'Welcome',
hello: 'Hello {{name}}!',
buttons: {
save: 'Save',
cancel: 'Cancel',
delete: 'Delete',
},
messages: {
success: 'Operation completed successfully',
error: 'An error occurred',
confirm: 'Are you sure you want to delete this item?',
},
navigation: {
home: 'Home',
about: 'About',
contact: 'Contact',
},
},
},
es: {
translation: {
welcome: 'Bienvenido',
hello: '¡Hola {{name}}!',
buttons: {
save: 'Guardar',
cancel: 'Cancelar',
delete: 'Eliminar',
},
messages: {
success: 'Operación completada exitosamente',
error: 'Ocurrió un error',
confirm: '¿Estás seguro de que quieres eliminar este elemento?',
},
navigation: {
home: 'Inicio',
about: 'Acerca de',
contact: 'Contacto',
},
},
},
};
i18n
.use(LanguageDetector)
.use(initReactI18next)
.init({
resources,
fallbackLng: 'en',
debug: process.env.NODE_ENV === 'development',
interpolation: {
escapeValue: false, // React already escapes values
},
detection: {
order: ['localStorage', 'navigator', 'htmlTag'],
caches: ['localStorage'],
},
});
export default i18n;
2. Using i18n in Components:
import { useTranslation } from 'react-i18next';
// Basic usage
const WelcomePage: React.FC = () => {
const { t, i18n } = useTranslation();
const changeLanguage = (lng: string) => {
i18n.changeLanguage(lng);
};
return (
<div>
<h1>{t('welcome')}</h1>
<p>{t('hello', { name: 'John' })}</p>
<div>
<button onClick={() => changeLanguage('en')}>English</button>
<button onClick={() => changeLanguage('es')}>Español</button>
</div>
</div>
);
};
// Advanced usage with namespaces
const UserProfile: React.FC = () => {
const { t } = useTranslation(['user', 'common']);
return (
<div>
<h2>{t('user:profile.title')}</h2>
<p>{t('user:profile.description')}</p>
<button>{t('common:buttons.save')}</button>
</div>
);
};
// Custom hook for complex translations
const useLocalizedDate = (date: Date) => {
const { i18n } = useTranslation();
return useMemo(() => {
return new Intl.DateTimeFormat(i18n.language, {
year: 'numeric',
month: 'long',
day: 'numeric',
}).format(date);
}, [date, i18n.language]);
};
3. Accessibility (a11y) Implementation:
// Accessible form component
const AccessibleForm: React.FC = () => {
const [formData, setFormData] = useState({
name: '',
email: '',
message: '',
});
const [errors, setErrors] = useState<Record<string, string>>({});
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
// Form validation logic
};
return (
<form onSubmit={handleSubmit} noValidate>
<fieldset>
<legend>Contact Information</legend>
<div className="form-group">
<label htmlFor="name" className="required">
Full Name
<span className="sr-only"> (required)</span>
</label>
<input
id="name"
type="text"
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
aria-describedby={errors.name ? 'name-error' : undefined}
aria-invalid={!!errors.name}
required
/>
{errors.name && (
<div id="name-error" role="alert" className="error-message">
{errors.name}
</div>
)}
</div>
<div className="form-group">
<label htmlFor="email" className="required">
Email Address
<span className="sr-only"> (required)</span>
</label>
<input
id="email"
type="email"
value={formData.email}
onChange={(e) => setFormData({ ...formData, email: e.target.value })}
aria-describedby={errors.email ? 'email-error' : undefined}
aria-invalid={!!errors.email}
required
/>
{errors.email && (
<div id="email-error" role="alert" className="error-message">
{errors.email}
</div>
)}
</div>
<div className="form-group">
<label htmlFor="message">Message</label>
<textarea
id="message"
value={formData.message}
onChange={(e) => setFormData({ ...formData, message: e.target.value })}
rows={4}
aria-describedby="message-help"
/>
<div id="message-help" className="help-text">
Please provide details about your inquiry.
</div>
</div>
<button type="submit" className="primary-button">
Send Message
</button>
</fieldset>
</form>
);
};
4. Accessible Navigation:
const AccessibleNavigation: React.FC = () => {
const [isMenuOpen, setIsMenuOpen] = useState(false);
const menuRef = useRef<HTMLUListElement>(null);
const buttonRef = useRef<HTMLButtonElement>(null);
const toggleMenu = () => {
setIsMenuOpen(!isMenuOpen);
};
const closeMenu = () => {
setIsMenuOpen(false);
buttonRef.current?.focus();
};
// Handle keyboard navigation
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === 'Escape') {
closeMenu();
}
};
// Handle focus management
useEffect(() => {
if (isMenuOpen && menuRef.current) {
const firstMenuItem = menuRef.current.querySelector('a');
firstMenuItem?.focus();
}
}, [isMenuOpen]);
return (
<nav role="navigation" aria-label="Main navigation">
<button
ref={buttonRef}
onClick={toggleMenu}
aria-expanded={isMenuOpen}
aria-controls="main-menu"
aria-haspopup="true"
className="menu-toggle"
>
<span className="sr-only">
{isMenuOpen ? 'Close' : 'Open'} main menu
</span>
<span aria-hidden="true">☰</span>
</button>
<ul
ref={menuRef}
id="main-menu"
className={`main-menu ${isMenuOpen ? 'open' : ''}`}
onKeyDown={handleKeyDown}
role="menubar"
>
<li role="none">
<a href="/" role="menuitem" tabIndex={isMenuOpen ? 0 : -1}>
Home
</a>
</li>
<li role="none">
<a href="/about" role="menuitem" tabIndex={isMenuOpen ? 0 : -1}>
About
</a>
</li>
<li role="none">
<a href="/contact" role="menuitem" tabIndex={isMenuOpen ? 0 : -1}>
Contact
</a>
</li>
</ul>
</nav>
);
};
5. Accessible Data Tables:
interface TableData {
id: string;
name: string;
email: string;
role: string;
status: 'active' | 'inactive';
}
const AccessibleTable: React.FC<{ data: TableData[] }> = ({ data }) => {
const [sortConfig, setSortConfig] = useState<{
key: keyof TableData;
direction: 'asc' | 'desc';
} | null>(null);
const handleSort = (key: keyof TableData) => {
setSortConfig(prev => ({
key,
direction: prev?.key === key && prev.direction === 'asc' ? 'desc' : 'asc',
}));
};
const sortedData = useMemo(() => {
if (!sortConfig) return data;
return [...data].sort((a, b) => {
const aVal = a[sortConfig.key];
const bVal = b[sortConfig.key];
if (aVal < bVal) return sortConfig.direction === 'asc' ? -1 : 1;
if (aVal > bVal) return sortConfig.direction === 'asc' ? 1 : -1;
return 0;
});
}, [data, sortConfig]);
return (
<div className="table-container">
<table role="table" aria-label="User data table">
<caption className="sr-only">
User data table with sortable columns
</caption>
<thead>
<tr role="row">
<th
role="columnheader"
tabIndex={0}
onClick={() => handleSort('name')}
onKeyDown={(e) => e.key === 'Enter' && handleSort('name')}
aria-sort={
sortConfig?.key === 'name'
? sortConfig.direction === 'asc' ? 'ascending' : 'descending'
: 'none'
}
>
Name
<span className="sr-only">
{sortConfig?.key === 'name'
? `Sorted ${sortConfig.direction === 'asc' ? 'ascending' : 'descending'}`
: 'Click to sort'}
</span>
</th>
<th
role="columnheader"
tabIndex={0}
onClick={() => handleSort('email')}
onKeyDown={(e) => e.key === 'Enter' && handleSort('email')}
aria-sort={
sortConfig?.key === 'email'
? sortConfig.direction === 'asc' ? 'ascending' : 'descending'
: 'none'
}
>
Email
</th>
<th
role="columnheader"
tabIndex={0}
onClick={() => handleSort('role')}
onKeyDown={(e) => e.key === 'Enter' && handleSort('role')}
aria-sort={
sortConfig?.key === 'role'
? sortConfig.direction === 'asc' ? 'ascending' : 'descending'
: 'none'
}
>
Role
</th>
<th
role="columnheader"
tabIndex={0}
onClick={() => handleSort('status')}
onKeyDown={(e) => e.key === 'Enter' && handleSort('status')}
aria-sort={
sortConfig?.key === 'status'
? sortConfig.direction === 'asc' ? 'ascending' : 'descending'
: 'none'
}
>
Status
</th>
</tr>
</thead>
<tbody>
{sortedData.map((row) => (
<tr key={row.id} role="row">
<td role="cell">{row.name}</td>
<td role="cell">{row.email}</td>
<td role="cell">{row.role}</td>
<td role="cell">
<span
className={`status-badge ${row.status}`}
aria-label={`Status: ${row.status}`}
>
{row.status}
</span>
</td>
</tr>
))}
</tbody>
</table>
</div>
);
};
6. Custom Accessibility Hooks:
// Focus management hook
const useFocusManagement = () => {
const focusableElements = useRef<HTMLElement[]>([]);
const trapFocus = (container: HTMLElement) => {
const focusable = container.querySelectorAll(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
) as NodeListOf<HTMLElement>;
focusableElements.current = Array.from(focusable);
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === 'Tab') {
const firstElement = focusableElements.current[0];
const lastElement = focusableElements.current[focusableElements.current.length - 1];
if (e.shiftKey) {
if (document.activeElement === firstElement) {
lastElement.focus();
e.preventDefault();
}
} else {
if (document.activeElement === lastElement) {
firstElement.focus();
e.preventDefault();
}
}
}
};
container.addEventListener('keydown', handleKeyDown);
return () => {
container.removeEventListener('keydown', handleKeyDown);
};
};
return { trapFocus };
};
// Screen reader announcements
const useScreenReaderAnnouncement = () => {
const announce = useCallback((message: string, priority: 'polite' | 'assertive' = 'polite') => {
const announcement = document.createElement('div');
announcement.setAttribute('aria-live', priority);
announcement.setAttribute('aria-atomic', 'true');
announcement.className = 'sr-only';
announcement.textContent = message;
document.body.appendChild(announcement);
setTimeout(() => {
document.body.removeChild(announcement);
}, 1000);
}, []);
return { announce };
};
7. Best Practices:
Internationalization:
- Use semantic keys for translations
- Support pluralization and interpolation
- Handle RTL languages properly
- Test with different text lengths
- Use proper date/number formatting
- Consider cultural differences in UI patterns
Accessibility:
- Use semantic HTML elements
- Provide proper ARIA labels and roles
- Ensure keyboard navigation works
- Maintain proper focus management
- Test with screen readers
- Use sufficient color contrast
- Provide alternative text for images
- Make interactive elements large enough
- Use proper heading hierarchy
- Test with real users with disabilities