DEV Community

Matthew Galbraith
Matthew Galbraith

Posted on

React Localization in 15 Minutes (Because Life's Too Short for Complex i18n)

React Localization in 15 Minutes (Because Life's Too Short for Complex i18n)

So I was building this React app last month and needed to add Spanish support. Figured it'd be quick - just translate some strings, right?

Wrong.

Spent 3 hours reading docs for various i18n libraries, configuring webpack loaders, setting up namespaces, and still couldn't get a simple "Hello" to show up in Spanish. There had to be a better way.

The Problem with Most i18n Solutions

Don't get me wrong - libraries like react-i18next are powerful. But for a lot of projects, they're overkill:

  • Complex setup with multiple config files
  • Learning curve for their specific syntax
  • Bundle size impact
  • Over-engineered for simple use cases

Sometimes you just want to fetch some translations and render them. That's it.

A Simpler Approach

Here's what I ended up building instead. The whole integration took about 10 minutes:

import { useState, useEffect } from 'react';

function App() {
 const [translations, setTranslations] = useState({});
 const [locale, setLocale] = useState('en');

 useEffect(() => {
   // Fetch translations from your API
   fetch(`/api/translations/${locale}`)
     .then(res => res.json())
     .then(data => setTranslations(data));
 }, [locale]);

 return (
   <div>
     <nav>
       <button onClick={() => setLocale('en')}>English</button>
       <button onClick={() => setLocale('es')}>Español</button>
     </nav>

     <h1>{translations.welcome || 'Welcome'}</h1>
     <p>{translations.description || 'Loading...'}</p>
   </div>
 );
}
Enter fullscreen mode Exit fullscreen mode

That's it. No providers, no namespaces, no special syntax. Just fetch JSON and use it.

The Backend Part

For the /api/translations/${locale} endpoint, I'm using Ling Arc which gives you clean JSON:

// pages/api/translations/[locale].js (Next.js)
export default async function handler(req, res) {
  const { locale } = req.query;

  const response = await fetch(
    'https://api.lingarc.com/projects/abc123/translations/export',
    { headers: { 'X-API-Key': process.env.LINGARC_API_KEY } }
  );

  const translations = await response.json();

  // Transform to flat object for the locale
  const localized = translations.reduce((acc, item) => {
    acc[item.key] = item.singular[locale] || item.singular.en;
    return acc;
  }, {});

  res.json(localized);
}
Enter fullscreen mode Exit fullscreen mode

You could use any translation service here - the point is keeping the React side simple.

Why This Works Better (For Me)

It's just JavaScript. No DSL to learn, no special components, no magic strings. If you know fetch() and useState, you're good.

Easy to debug. When something breaks, you can just console.log(translations) and see exactly what you're working with.

Flexible. Want to cache translations? Add caching. Need pluralization? Handle it however you want. It's just data.

Small bundle. No extra dependencies beyond what React already gives you.

Making It Reusable

Here's a quick hook to clean it up:

function useTranslations(locale) {
  const [translations, setTranslations] = useState({});
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    setLoading(true);
    fetch(`/api/translations/${locale}`)
      .then(res => res.json())
      .then(data => {
        setTranslations(data);
        setLoading(false);
      })
      .catch(() => setLoading(false));
  }, [locale]);

  return { translations, loading };
}

// Usage
function MyComponent() {
  const { translations, loading } = useTranslations('es');

  if (loading) return <div>Loading...</div>;

  return <h1>{translations.greeting}</h1>;
}
Enter fullscreen mode Exit fullscreen mode

When NOT to Use This

This approach works great for:

  • Small to medium apps
  • Simple translation needs
  • Teams that prefer explicit over magical

But stick with react-i18next if you need:

  • Complex pluralization rules
  • Interpolation with rich formatting
  • Lazy loading of translation chunks
  • ICU message format

Wrapping Up

Sometimes the simplest solution is just fetching JSON and using it. No shame in keeping things straightforward.

The whole setup took me 15 minutes vs the hours I spent wrestling with more complex solutions. Sometimes that's all you need.

Top comments (0)