Web Animations: Framer Motion and CSS for React Developers
A practical guide to animations in React — CSS transitions and keyframes, Framer Motion for complex interactions, performance optimization, and accessibility considerations.
A practical guide to internationalizing React applications — setting up translations, handling plurals, formatting dates and numbers, and implementing language switching.
By codefunc
Internationalization (i18n) prepares your app for multiple languages. Localization (l10n) adapts it for specific locales. Even if you're launching in one language, building with i18n from the start is easier than retrofitting later. React has mature tools for this — react-i18next is the standard choice.
Bottom line: Use react-i18next for React apps, next-intl for Next.js App Router. Extract all user-facing strings to translation files. Handle plurals, interpolation, and context properly. Format dates and numbers with the Intl API. Store language preference and sync with URL for SEO. Plan for text expansion — German is ~30% longer than English.
npm install i18next react-i18next i18next-browser-languagedetector
// lib/i18n.ts
import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
import LanguageDetector from 'i18next-browser-languagedetector';
import en from '../locales/en/translation.json';
import es from '../locales/es/translation.json';
import de from '../locales/de/translation.json';
i18n
.use(LanguageDetector)
.use(initReactI18next)
.init({
resources: {
en: { translation: en },
es: { translation: es },
de: { translation: de },
},
fallbackLng: 'en',
interpolation: {
escapeValue: false, // React already escapes
},
detection: {
order: ['localStorage', 'navigator'],
caches: ['localStorage'],
},
});
export default i18n;
// locales/en/translation.json
{
"common": {
"save": "Save",
"cancel": "Cancel",
"delete": "Delete",
"loading": "Loading..."
},
"auth": {
"login": "Log in",
"logout": "Log out",
"signUp": "Sign up",
"forgotPassword": "Forgot password?"
},
"dashboard": {
"welcome": "Welcome, {{name}}!",
"lastLogin": "Last login: {{date}}"
}
}
// locales/es/translation.json
{
"common": {
"save": "Guardar",
"cancel": "Cancelar",
"delete": "Eliminar",
"loading": "Cargando..."
},
"auth": {
"login": "Iniciar sesión",
"logout": "Cerrar sesión",
"signUp": "Registrarse",
"forgotPassword": "¿Olvidaste tu contraseña?"
},
"dashboard": {
"welcome": "¡Bienvenido, {{name}}!",
"lastLogin": "Último acceso: {{date}}"
}
}
// app/layout.tsx or main.tsx
import '@/lib/i18n';
// In React
import { I18nextProvider } from 'react-i18next';
import i18n from '@/lib/i18n';
function App({ children }) {
return (
<I18nextProvider i18n={i18n}>
{children}
</I18nextProvider>
);
}
import { useTranslation } from 'react-i18next';
function Dashboard() {
const { t } = useTranslation();
return (
<div>
<h1>{t('dashboard.welcome', { name: 'Ada' })}</h1>
<button>{t('common.save')}</button>
<button>{t('common.cancel')}</button>
</div>
);
}
For translations with JSX:
import { Trans } from 'react-i18next';
function Terms() {
return (
<p>
<Trans i18nKey="terms.agreement">
By signing up, you agree to our <a href="/terms">Terms</a> and{' '}
<a href="/privacy">Privacy Policy</a>.
</Trans>
</p>
);
}
{
"terms": {
"agreement": "By signing up, you agree to our <0>Terms</0> and <1>Privacy Policy</1>."
}
}
{
"items": {
"count_one": "{{count}} item",
"count_other": "{{count}} items"
},
"messages": {
"unread_zero": "No unread messages",
"unread_one": "1 unread message",
"unread_other": "{{count}} unread messages"
}
}
t('items.count', { count: 1 }); // "1 item"
t('items.count', { count: 5 }); // "5 items"
t('messages.unread', { count: 0 }); // "No unread messages"
Some languages have multiple plural forms:
// locales/ru/translation.json
{
"items": {
"count_one": "{{count}} предмет",
"count_few": "{{count}} предмета",
"count_many": "{{count}} предметов"
}
}
{
"greeting": "Hello, {{name}}!",
"orderStatus": "Your order #{{orderId}} is {{status}}"
}
t('greeting', { name: 'Ada' });
t('orderStatus', { orderId: '12345', status: 'shipped' });
{
"price": "Price: {{value, currency}}",
"date": "Date: {{date, datetime}}"
}
// Configure formatters in i18n.init()
i18n.init({
interpolation: {
format: (value, format, lng) => {
if (format === 'currency') {
return new Intl.NumberFormat(lng, {
style: 'currency',
currency: 'USD',
}).format(value);
}
if (format === 'datetime') {
return new Intl.DateTimeFormat(lng).format(value);
}
return value;
},
},
});
Different translations based on context:
{
"friend": "A friend",
"friend_male": "A boyfriend",
"friend_female": "A girlfriend"
}
t('friend', { context: 'male' }); // "A boyfriend"
t('friend', { context: 'female' }); // "A girlfriend"
import { useTranslation } from 'react-i18next';
function LanguageSwitcher() {
const { i18n } = useTranslation();
const languages = [
{ code: 'en', name: 'English' },
{ code: 'es', name: 'Español' },
{ code: 'de', name: 'Deutsch' },
];
return (
<select
value={i18n.language}
onChange={(e) => i18n.changeLanguage(e.target.value)}
>
{languages.map((lang) => (
<option key={lang.code} value={lang.code}>
{lang.name}
</option>
))}
</select>
);
}
next-intl is designed for Next.js App Router.
npm install next-intl
// i18n.ts
import { getRequestConfig } from 'next-intl/server';
export default getRequestConfig(async ({ locale }) => ({
messages: (await import(`./messages/${locale}.json`)).default,
}));
// middleware.ts
import createMiddleware from 'next-intl/middleware';
export default createMiddleware({
locales: ['en', 'es', 'de'],
defaultLocale: 'en',
});
export const config = {
matcher: ['/', '/(en|es|de)/:path*'],
};
app/
[locale]/
layout.tsx
page.tsx
about/
page.tsx
messages/
en.json
es.json
de.json
// app/[locale]/layout.tsx
import { NextIntlClientProvider } from 'next-intl';
import { getMessages } from 'next-intl/server';
export default async function LocaleLayout({
children,
params: { locale },
}: {
children: React.ReactNode;
params: { locale: string };
}) {
const messages = await getMessages();
return (
<html lang={locale}>
<body>
<NextIntlClientProvider messages={messages}>
{children}
</NextIntlClientProvider>
</body>
</html>
);
}
// Server Component
import { getTranslations } from 'next-intl/server';
export default async function HomePage() {
const t = await getTranslations('home');
return <h1>{t('title')}</h1>;
}
// Client Component
'use client';
import { useTranslations } from 'next-intl';
export default function Button() {
const t = useTranslations('common');
return <button>{t('save')}</button>;
}
'use client';
import { usePathname, useRouter } from 'next/navigation';
import { useLocale } from 'next-intl';
export default function LanguageSwitcher() {
const locale = useLocale();
const pathname = usePathname();
const router = useRouter();
const switchLocale = (newLocale: string) => {
const newPath = pathname.replace(`/${locale}`, `/${newLocale}`);
router.push(newPath);
};
return (
<select value={locale} onChange={(e) => switchLocale(e.target.value)}>
<option value="en">English</option>
<option value="es">Español</option>
<option value="de">Deutsch</option>
</select>
);
}
Use the Intl API for locale-aware formatting:
function FormattedDate({ date, locale }) {
const formatted = new Intl.DateTimeFormat(locale, {
dateStyle: 'long',
timeStyle: 'short',
}).format(date);
return <time dateTime={date.toISOString()}>{formatted}</time>;
}
// "July 7, 2026 at 2:30 PM" (en-US)
// "7 de julio de 2026, 14:30" (es-ES)
// "7. Juli 2026 um 14:30" (de-DE)
function FormattedNumber({ value, locale }) {
const formatted = new Intl.NumberFormat(locale).format(value);
return <span>{formatted}</span>;
}
// 1,234,567.89 (en-US)
// 1.234.567,89 (de-DE)
// 1 234 567,89 (fr-FR)
function Price({ amount, currency, locale }) {
const formatted = new Intl.NumberFormat(locale, {
style: 'currency',
currency,
}).format(amount);
return <span>{formatted}</span>;
}
// $1,234.56 (en-US, USD)
// 1.234,56 € (de-DE, EUR)
// £1,234.56 (en-GB, GBP)
function TimeAgo({ date, locale }) {
const rtf = new Intl.RelativeTimeFormat(locale, { numeric: 'auto' });
const diff = (date.getTime() - Date.now()) / 1000;
if (Math.abs(diff) < 60) return rtf.format(Math.round(diff), 'second');
if (Math.abs(diff) < 3600) return rtf.format(Math.round(diff / 60), 'minute');
if (Math.abs(diff) < 86400) return rtf.format(Math.round(diff / 3600), 'hour');
return rtf.format(Math.round(diff / 86400), 'day');
}
// "2 hours ago" (en)
// "hace 2 horas" (es)
// "vor 2 Stunden" (de)
// Good: hierarchical, descriptive
{
"pages": {
"home": {
"hero": {
"title": "Welcome to Our App",
"subtitle": "The best solution for..."
}
}
},
"components": {
"button": {
"submit": "Submit",
"cancel": "Cancel"
}
}
}
// Avoid: flat, unclear
{
"title1": "Welcome",
"btn1": "Submit"
}
Split translations by feature:
i18n.init({
ns: ['common', 'auth', 'dashboard', 'settings'],
defaultNS: 'common',
});
const { t } = useTranslation('auth');
t('login'); // Uses auth namespace
i18n.init({
saveMissing: process.env.NODE_ENV === 'development',
missingKeyHandler: (lng, ns, key) => {
console.warn(`Missing translation: ${lng}/${ns}/${key}`);
},
});
German text is ~30% longer than English. Design UI to accommodate:
/* Avoid fixed widths */
.button {
padding: 0.5rem 1rem;
white-space: nowrap;
/* NOT: width: 100px; */
}
Use the Text Diff tool to compare translation files and find missing keys. Validate JSON syntax with JSON Validator. Format messy translation files with JSON Formatter.
npm install -D i18next-parser
// i18next-parser.config.js
module.exports = {
locales: ['en', 'es', 'de'],
output: 'locales/$LOCALE/$NAMESPACE.json',
input: ['src/**/*.{ts,tsx}'],
keySeparator: '.',
namespaceSeparator: ':',
};
// In <head>
<link rel="alternate" hreflang="en" href="https://example.com/en/about" />
<link rel="alternate" hreflang="es" href="https://example.com/es/about" />
<link rel="alternate" hreflang="de" href="https://example.com/de/about" />
<link rel="alternate" hreflang="x-default" href="https://example.com/about" />
<html lang={locale}>
<url>
<loc>https://example.com/en/about</loc>
<xhtml:link rel="alternate" hreflang="en" href="https://example.com/en/about"/>
<xhtml:link rel="alternate" hreflang="es" href="https://example.com/es/about"/>
<xhtml:link rel="alternate" hreflang="de" href="https://example.com/de/about"/>
</url>
Start with i18n structure even for single-language apps — it's much harder to add later. Use react-i18next for client-side React, next-intl for Next.js App Router. Organize translations hierarchically by feature. Handle plurals properly for each language. Use Intl API for dates and numbers. Put locale in URL for SEO. Plan UI for text expansion. Automate key extraction to keep translations in sync.
A practical guide to animations in React — CSS transitions and keyframes, Framer Motion for complex interactions, performance optimization, and accessibility considerations.
A practical guide to authentication for frontend developers — understanding OAuth flows, JWT structure, session cookies, and implementing secure auth in React applications.
A practical guide to using GraphQL in frontend applications — writing queries and mutations, client-side caching with Apollo and urql, and patterns for React integration.
Find a developer tool