Client Components
This guide explains how to use the internationalization package with React Client Components in your Next.js application.
Overview
Client Components run in the browser and can include interactive features. To use translations in Client Components, you need to:
- Wrap your Client Components with the
TranslationProvider
in a Server Component
- Use the
useTranslation
hook in your Client Components to access translations
Setting Up the Translation Provider
In a Server Component that renders your Client Component, wrap the Client Component with the TranslationProvider
:
// app/[locale]/contact/page.tsx (Server Component)
import { getDictionary } from '@repo/internationalization';
import { TranslationProvider } from '@repo/internationalization/TranslationProvider';
import ContactForm from '@/components/ContactForm'; // Client component
export default async function ContactPage({ params }: { params: { locale: string } }) {
const dictionary = await getDictionary(params.locale);
return (
<div>
<h1>{dictionary.web.contact.title}</h1>
<p>{dictionary.web.contact.description}</p>
<TranslationProvider locale={params.locale} messages={dictionary}>
<ContactForm />
</TranslationProvider>
</div>
);
}
The TranslationProvider
component:
- Takes the current locale and dictionary as props
- Makes translations available to all Client Components within its subtree
- Uses
next-intl
under the hood to provide translations
Using the useTranslation Hook
In your Client Components, import and use the useTranslation
hook to access translations:
// components/ContactForm.tsx (Client Component)
'use client';
import { useState } from 'react';
import { useTranslation } from '@repo/internationalization/useTranslation';
export default function ContactForm() {
const { t } = useTranslation('web.contact.form');
const [formData, setFormData] = useState({
name: '',
email: '',
message: ''
});
const handleChange = (e) => {
const { name, value } = e.target;
setFormData(prev => ({ ...prev, [name]: value }));
};
const handleSubmit = (e) => {
e.preventDefault();
// Form submission logic
};
return (
<form onSubmit={handleSubmit}>
<div className="form-group">
<label htmlFor="name">{t('name')}</label>
<input
type="text"
id="name"
name="name"
value={formData.name}
onChange={handleChange}
placeholder={t('namePlaceholder')}
required
/>
</div>
<div className="form-group">
<label htmlFor="email">{t('email')}</label>
<input
type="email"
id="email"
name="email"
value={formData.email}
onChange={handleChange}
placeholder={t('emailPlaceholder')}
required
/>
</div>
<div className="form-group">
<label htmlFor="message">{t('message')}</label>
<textarea
id="message"
name="message"
value={formData.message}
onChange={handleChange}
placeholder={t('messagePlaceholder')}
required
></textarea>
</div>
<button type="submit">{t('submit')}</button>
</form>
);
}
The useTranslation
hook:
- Takes a namespace parameter that specifies which part of the translation object to use
- Returns a
t
function that you can use to access translations within that namespace
- Automatically updates when the locale changes
Namespace Parameter
The namespace parameter in the useTranslation
hook helps you access a specific section of your translations:
// Access translations under web.contact.form
const { t } = useTranslation('web.contact.form');
// Now you can use t('fieldName') instead of t('web.contact.form.fieldName')
t('name') // -> "Name"
t('email') // -> "Email"
You can use different namespaces in different components based on the section of translations they need:
// In a navigation component
const { t } = useTranslation('web.header');
// In a footer component
const { t } = useTranslation('web.footer');
You can include dynamic values in your translations:
// Translation: "Hello, {name}!"
const { t } = useTranslation('web.greeting');
// Pass values as an object
t('welcome', { name: user.name }) // -> "Hello, John!"
For date and number formatting:
// Translation: "Last updated on {date}"
const { t } = useTranslation('web.common');
// Format date according to the current locale
const formattedDate = new Intl.DateTimeFormat(locale, {
year: 'numeric',
month: 'long',
day: 'numeric'
}).format(new Date());
t('lastUpdated', { date: formattedDate })
Pluralization
You can handle pluralization in your translations:
// In your translation file:
// {
// "items": {
// "zero": "No items",
// "one": "One item",
// "other": "{count} items"
// }
// }
const { t } = useTranslation('web.products');
// Pass the count as a parameter
t('items', { count: 0 }) // -> "No items"
t('items', { count: 1 }) // -> "One item"
t('items', { count: 5 }) // -> "5 items"
Creating a Language Switcher
You can create a language switcher component to allow users to change the language:
// components/LanguageSwitcher.tsx
'use client';
import Link from 'next/link';
import { usePathname } from 'next/navigation';
import { i18nConfig } from '@repo/internationalization/i18nConfig';
import { useTranslation } from '@repo/internationalization/useTranslation';
// Map of locale codes to their display names
const localeNames: Record<string, string> = {
en: 'English',
tr: 'Türkçe',
es: 'Español',
de: 'Deutsch'
};
export default function LanguageSwitcher() {
const pathname = usePathname();
const { t } = useTranslation('web.common');
// Function to get the path for a different locale
const getLocalePath = (locale: string) => {
const segments = pathname.split('/');
const currentLocale = segments[1];
// Check if the current path already has a locale
if (i18nConfig.locales.includes(currentLocale)) {
segments[1] = locale;
return segments.join('/');
}
// If no locale in the path, add it
return `/${locale}${pathname}`;
};
return (
<div className="language-switcher">
<span className="language-label">{t('language')}:</span>
<ul className="language-list">
{i18nConfig.locales.map((locale) => (
<li key={locale}>
<Link
href={getLocalePath(locale)}
className={pathname.startsWith(`/${locale}/`) ? 'active' : ''}
>
{localeNames[locale] || locale.toUpperCase()}
</Link>
</li>
))}
</ul>
</div>
);
}
Best Practices
Examples
// components/SignupForm.tsx
'use client';
import { useState } from 'react';
import { useTranslation } from '@repo/internationalization/useTranslation';
type FormErrors = {
name?: string;
email?: string;
password?: string;
};
export default function SignupForm() {
const { t } = useTranslation('web.auth.signup');
const [formData, setFormData] = useState({
name: '',
email: '',
password: ''
});
const [errors, setErrors] = useState<FormErrors>({});
const validate = () => {
const newErrors: FormErrors = {};
if (!formData.name) {
newErrors.name = t('errors.nameRequired');
}
if (!formData.email) {
newErrors.email = t('errors.emailRequired');
} else if (!/\S+@\S+\.\S+/.test(formData.email)) {
newErrors.email = t('errors.emailInvalid');
}
if (!formData.password) {
newErrors.password = t('errors.passwordRequired');
} else if (formData.password.length < 8) {
newErrors.password = t('errors.passwordLength');
}
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
const handleChange = (e) => {
const { name, value } = e.target;
setFormData(prev => ({ ...prev, [name]: value }));
// Clear error when user starts typing
if (errors[name]) {
setErrors(prev => ({ ...prev, [name]: undefined }));
}
};
const handleSubmit = (e) => {
e.preventDefault();
if (validate()) {
// Form submission logic
console.log('Form submitted:', formData);
alert(t('submitSuccess'));
}
};
return (
<form onSubmit={handleSubmit} className="signup-form">
<h2>{t('title')}</h2>
<div className="form-group">
<label htmlFor="name">{t('name')}</label>
<input
type="text"
id="name"
name="name"
value={formData.name}
onChange={handleChange}
className={errors.name ? 'error' : ''}
/>
{errors.name && <span className="error-message">{errors.name}</span>}
</div>
<div className="form-group">
<label htmlFor="email">{t('email')}</label>
<input
type="email"
id="email"
name="email"
value={formData.email}
onChange={handleChange}
className={errors.email ? 'error' : ''}
/>
{errors.email && <span className="error-message">{errors.email}</span>}
</div>
<div className="form-group">
<label htmlFor="password">{t('password')}</label>
<input
type="password"
id="password"
name="password"
value={formData.password}
onChange={handleChange}
className={errors.password ? 'error' : ''}
/>
{errors.password && <span className="error-message">{errors.password}</span>}
</div>
<div className="form-actions">
<button type="submit">{t('submit')}</button>
</div>
<p className="terms">{t('termsNotice')}</p>
</form>
);
}
Dynamic Content Loading
// components/DynamicContent.tsx
'use client';
import { useState, useEffect } from 'react';
import { useTranslation } from '@repo/internationalization/useTranslation';
export default function DynamicContent() {
const { t } = useTranslation('web.dashboard');
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
async function fetchData() {
try {
setLoading(true);
const response = await fetch('/api/data');
const result = await response.json();
setData(result);
setError(null);
} catch (err) {
setError(t('errors.fetchFailed'));
} finally {
setLoading(false);
}
}
fetchData();
}, [t]);
if (loading) {
return <div className="loading">{t('loading')}</div>;
}
if (error) {
return <div className="error">{error}</div>;
}
return (
<div className="dynamic-content">
<h2>{t('dataTitle')}</h2>
{data && data.items && data.items.length > 0 ? (
<ul className="data-list">
{data.items.map((item, index) => (
<li key={index} className="data-item">
<h3>{item.title}</h3>
<p>{item.description}</p>
<span className="date">
{t('lastUpdated', {
date: new Date(item.updatedAt).toLocaleDateString()
})}
</span>
</li>
))}
</ul>
) : (
<p className="no-data">{t('noData')}</p>
)}
</div>
);
}
Next Steps