DEV Community

Ademola Thompson
Ademola Thompson

Posted on

How to make your React app multilingual

When you build a React application for the first time, your UI usually speaks only one language (e.g., English, French). However, as your user base grows, you might consider making your app available in the languages your users prefer. How do you handle this without rewriting or restructuring your entire code?

In this tutorial, you will build a simple React application and learn how to localize and translate it without breaking or restructuring your code.

Prerequisites

Before you start, you will need:

  • Node.js 18+
  • Understanding of JavaScript/ReactJS

Building a React application

In this section, you will build a simple React application with 4 pages:

  • Home
  • About
  • Contact
  • Notifications

If you want to skip this part and go straight to the localization section, you can clone the starter template on GitHub. Otherwise, let’s start building!

Step 1: Initialize a React project

Open your command line interface (CLI) on your computer and type this command to create a ReactJS application with Vite:

npm create vite@latest my-app -- --template react-ts
Enter fullscreen mode Exit fullscreen mode

The command above will create a new project called my-app and use the typescript template for it. Once done, navigate into my-app and install dependencies:

cd my-app && npm install
Enter fullscreen mode Exit fullscreen mode

Now, you can optionally open your application in VSCode:

code .
Enter fullscreen mode Exit fullscreen mode

Finally, you can start your React application by typing this from your root folder (my-app) in the CLI:

npm run dev
Enter fullscreen mode Exit fullscreen mode

You will see a page like this when you visit http://localhost:5173/:

vite and react icons showing on page

Step 2: Install dependencies and configure your project

Now that you’ve created an empty React project, you need to install some additional dependencies and then update some of the files.

  1. Start by installing a package for routing. Type this into your CLI from your project root folder (my-app):

    npm install react-router-dom
    

    This will install React Router, which the project will use for routing your app to different pages.

  2. Next, update your main.tsx file to use BrowserRouter from react-router-dom. This will enable client-side routing of different components/pages without a full page reload:

    import { StrictMode } from "react";
    import { createRoot } from "react-dom/client";
    import { BrowserRouter } from "react-router-dom";
    import App from "./App";
    import "./index.css";
    
    createRoot(document.getElementById("root")!).render(
      <StrictMode>
        <BrowserRouter>
          <App />
        </BrowserRouter>
      </StrictMode>
    );
    
  3. Now, update your App.tsx file:

    import { Routes, Route } from "react-router-dom";
    import { Navbar } from "./components/Navbar";
    import { Footer } from "./components/Footer";
    import { Home } from "./pages/Home";
    import { About } from "./pages/About";
    import { Contact } from "./pages/Contact";
    import { Notifications } from "./pages/Notifications";
    import "./App.css";
    
    export default function App() {
      return (
        <div className="app">
          <Navbar />
          <main>
            <Routes>
              <Route path="/" element={<Home />} />
              <Route path="/about" element={<About />} />
              <Route path="/contact" element={<Contact />} />
              <Route path="/notifications" element={<Notifications />} />
            </Routes>
          </main>
          <Footer />
        </div>
      );
    }
    

    This renders the Navbar and Footer on every page, and uses Routes to map each URL path to its corresponding page component, so visiting /about renders the About component, /contact renders Contact, and so on. You will create the components and pages in the next section.

  4. Finally, update your CSS files. Update App.css like this:

    .app {
      min-height: 100vh;
      display: flex;
      flex-direction: column;
    }
    
    main {
      flex: 1;
    }
    

    Update index.css like this:

    *, *::before, *::after {
      box-sizing: border-box;
      margin: 0;
      padding: 0;
    }
    
    body {
      font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
      color: #111;
      background: #fff;
      line-height: 1.6;
    }
    
    a {
      text-decoration: none;
      color: inherit;
    }
    

Congratulations! If you have completed this section, you are ready to build your components.

Step 3: Build your navbar and footer components

In your src folder, create a folder called components. This will hold your navbar and footer components.

Now, create two files called Navbar.tsx and Footer.tsx inside the Components folder.

Navbar.tsx:

// src/components/Navbar.tsx
import { Link } from "react-router-dom";
import "./Navbar.css";

export function Navbar() {
  return (
    <nav className="navbar">
      <Link to="/" className="logo">
        MyApp
      </Link>
      <ul>
        <li>
          <Link to="/">Home</Link>
        </li>
        <li>
          <Link to="/about">About</Link>
        </li>
        <li>
          <Link to="/contact">Contact</Link>
        </li>
        <li>
          <Link to="/notifications">Notifications</Link>
        </li>
      </ul>
    </nav>
  );
}
Enter fullscreen mode Exit fullscreen mode

Footer.tsx:

// src/components/Footer.tsx
import "./Footer.css";

export function Footer() {
  return (
    <footer className="footer">
      <p>© 2026 MyApp. All rights reserved.</p>
    </footer>
  );
}
Enter fullscreen mode Exit fullscreen mode

Now, you can style both components however you want. The following CSS files provide simple styles you can use. Create them inside your components folder.

Navbar.css:

/* src/components/Navbar.css */
.navbar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 1rem 2rem;
  border-bottom: 1px solid #eee;
}

.logo {
  font-weight: 700;
  font-size: 1.2rem;
  color: #111;
  text-decoration: none;
}

.navbar ul {
  list-style: none;
  display: flex;
  gap: 1.5rem;
}

.navbar a {
  text-decoration: none;
  color: #444;
  font-size: 0.95rem;
}

.navbar a:hover {
  color: #000;
}
Enter fullscreen mode Exit fullscreen mode

Footer.css:

/* src/components/Footer.css */
.footer {
  padding: 1.5rem 2rem;
  border-top: 1px solid #eee;
  text-align: center;
  color: #999;
  font-size: 0.85rem;
}
Enter fullscreen mode Exit fullscreen mode

Once you are done, you will be ready to build actual pages for your application.

Step 4: Create components for your pages

Start by creating a new folder inside your existing src folder. Call it pages. It will hold your app’s pages and their related styles. Inside this new folder, create the following files:

  • Home.tsx
  • About.tsx
  • Contact.tsx
  • Notifications.tsx

Now, update each of the files with some code:

Home.tsx:

// src/pages/Home.tsx
import "./Home.css";

export function Home() {
  return (
    <div className="home">
      <section className="hero">
        <h1>Build something people love</h1>
        <p>We help teams ship faster, collaborate better, and grow with confidence. Start for free — no credit card required.
        </p>
        <button>Get started</button>
      </section>

      <section className="features">
        <div className="card">
          <h3>Fast by default</h3>
          <p>Optimized for performance from day one so your users never wait.</p>
        </div>
        <div className="card">
          <h3>Easy to use</h3>
          <p>A clean interface your whole team can pick up without training.</p>
        </div>
        <div className="card">
          <h3>Built to scale</h3>
          <p>Grows with you from your first user to your millionth.</p>
        </div>
      </section>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

About.tsx:

// src/pages/About.tsx
import "./About.css";

export function About() {
  return (
    <div className="about">
      <h1>About us</h1>
      <p>We are a small team of developers and designers who believe that great software should be accessible to everyone, regardless of where they are in the world.
      </p>
      <div className="team">
        <div className="member">
          <div className="avatar">JD</div>
          <h3>Jane Doe</h3>
          <span>Co-founder & CEO</span>
          <p>
            Jane leads product and strategy with over 10 years of experience building developer tools.
          </p>
        </div>
        <div className="member">
          <div className="avatar">JS</div>
          <h3>John Smith</h3>
          <span>Co-founder & CTO</span>
          <p>
            John oversees engineering and architecture, previously at several Y Combinator startups.
          </p>
        </div>
        <div className="member">
          <div className="avatar">AP</div>
          <h3>Aisha Patel</h3>
          <span>Head of Design</span>
          <p>
            Aisha crafts the product experience with a background in design systems and accessibility.
          </p>
        </div>
      </div>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Contact.tsx:

// src/pages/Contact.tsx
import { useState } from "react";
import "./Contact.css";

export function Contact() {
  const [name, setName] = useState("");
  const [email, setEmail] = useState("");
  const [message, setMessage] = useState("");
  const [submitted, setSubmitted] = useState(false);

  function handleSubmit() {
    if (!name || !email || !message) return;
    setSubmitted(true);
    setName("");
    setEmail("");
    setMessage("");
    setTimeout(() => setSubmitted(false), 3000);
  }
  return (
    <div className="contact">
      {submitted && <div className="toast">Message sent successfully!</div>}
      <h1>Contact us</h1>
      <p>
        Have a question or want to work together? We would love to hear from you.
      </p>
      <div className="form">
        <div className="field">
          <label htmlFor="name">Your name</label>
          <input
            id="name"
            type="text"
            placeholder="Jane Doe"
            value={name}
            onChange={(e) => setName(e.target.value)}
          />
        </div>
        <div className="field">
          <label htmlFor="email">Email address</label>
          <input
            id="email"
            type="email"
            placeholder="jane@example.com"
            value={email}
            onChange={(e) => setEmail(e.target.value)}
          />
        </div>
        <div className="field">
          <label htmlFor="message">Message</label>
          <textarea
            id="message"
            rows={5}
            placeholder="Tell us what's on your mind..."
            value={message}
            onChange={(e) => setMessage(e.target.value)}
          />
        </div>
        <button onClick={handleSubmit}>Send message</button>
      </div>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Notifications.tsx:

//src/pages/Notifications.tsx
import "./Notifications.css";

const notifications = [
  {
    id: 1,
    user: "Alice Martin",
    action: "joined the team",
    date: new Date("2025-11-03"),
  },
  {
    id: 2,
    user: "Bob Chen",
    action: "left a comment on your post",
    date: new Date("2025-11-05"),
  },
];

const dateFormatter = new Intl.DateTimeFormat(navigator.language, {
  year: "2-digit",
  month: "2-digit",
  day: "2-digit",
});

export function Notifications() {
  return (
    <div className="notifications">
      <h1>Notifications</h1>
      <ul className="notification-list">
        {notifications.map((a) => (
          <li key={a.id} className="notification-item">
            <div className="notification-avatar">
              {a.user === "System" ? "S" : a.user[0]}
            </div>
            <div className="notification-body">
              <p>
                <strong>{a.user}</strong> {a.action}
              </p>
              <span className="notification-date">
                {dateFormatter.format(a.date)}
              </span>
            </div>
          </li>
        ))}
      </ul>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Congratulations! You have created four new pages in your React application, including a contact page with a form, and a notifications page with some hard-coded notifications.

Now, you should style your pages. Create the following files inside your pages folder:

  • Home.css
  • About.css
  • Contact.css
  • Notifications.css

You can update the styling of your pages however you want. The following are simple styles you can use.

Home.css:

/* src/pages/Home.css */
.home {
  max-width: 900px;
  margin: 0 auto;
  padding: 0 2rem 4rem;
}

.hero {
  text-align: center;
  padding: 5rem 2rem 3rem;
}

.hero h1 {
  font-size: 2.2rem;
  font-weight: 800;
  line-height: 1.2;
  margin-bottom: 1rem;
}

.hero p {
  color: #555;
  font-size: 1.05rem;
  margin-bottom: 2rem;
  max-width: 520px;
  margin-left: auto;
  margin-right: auto;
}

.hero button {
  background: #000;
  color: #fff;
  border: none;
  padding: 0.75rem 1.75rem;
  font-size: 1rem;
  border-radius: 6px;
  cursor: pointer;
}

.hero button:hover {
  background: #222;
}

.features {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
  gap: 1.5rem;
}

.card {
  padding: 1.5rem;
  border: 1px solid #eee;
  border-radius: 8px;
}

.card h3 {
  margin-bottom: 0.5rem;
  font-size: 1rem;
  font-weight: 600;
}

.card p {
  color: #555;
  font-size: 0.9rem;
}
Enter fullscreen mode Exit fullscreen mode

About.css:

/* src/pages/About.css */
.about {
  max-width: 800px;
  margin: 0 auto;
  padding: 3rem 2rem 4rem;
}

.about h1 {
  font-size: 2rem;
  font-weight: 800;
  margin-bottom: 1rem;
}

.about > p {
  color: #555;
  font-size: 1.05rem;
  margin-bottom: 3rem;
  line-height: 1.8;
  max-width: 580px;
}

.team {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
  gap: 1.5rem;
}

.member {
  padding: 1.5rem;
  border: 1px solid #eee;
  border-radius: 8px;
}

.avatar {
  width: 40px;
  height: 40px;
  border-radius: 50%;
  background: #111;
  color: #fff;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 0.8rem;
  font-weight: 700;
  margin-bottom: 0.75rem;
}

.member h3 {
  font-size: 0.95rem;
  font-weight: 600;
  margin-bottom: 0.2rem;
}

.member span {
  font-size: 0.8rem;
  color: #888;
  display: block;
  margin-bottom: 0.6rem;
}

.member p {
  font-size: 0.85rem;
  color: #555;
  line-height: 1.6;
}
Enter fullscreen mode Exit fullscreen mode

Contact.css:

/* src/pages/Contact.css */
.contact {
  max-width: 520px;
  margin: 0 auto;
  padding: 3rem 2rem 4rem;
}

.contact h1 {
  font-size: 2rem;
  font-weight: 800;
  margin-bottom: 0.5rem;
}

.contact > p {
  color: #555;
  font-size: 1.05rem;
  margin-bottom: 2rem;
}

.form {
  display: flex;
  flex-direction: column;
  gap: 1.25rem;
}

.field {
  display: flex;
  flex-direction: column;
  gap: 0.4rem;
}

.field label {
  font-size: 0.9rem;
  font-weight: 500;
}

.field input,
.field textarea {
  padding: 0.65rem 0.85rem;
  border: 1px solid #ddd;
  border-radius: 6px;
  font-size: 0.95rem;
  font-family: inherit;
  color: #111;
  resize: vertical;
}

.field input:focus,
.field textarea:focus {
  outline: none;
  border-color: #000;
}

button {
  background: #000;
  color: #fff;
  border: none;
  padding: 0.75rem 1.5rem;
  font-size: 1rem;
  border-radius: 6px;
  cursor: pointer;
  align-self: flex-start;
}

button:hover {
  background: #222;
}

.toast {
  position: fixed;
  bottom: 2rem;
  right: 2rem;
  background: #111;
  color: #fff;
  padding: 0.85rem 1.25rem;
  border-radius: 8px;
  font-size: 0.9rem;
  font-weight: 500;
  animation: slideUp 0.2s ease;
}

@keyframes slideUp {
  from {
    transform: translateY(10px);
    opacity: 0;
  }
  to {
    transform: translateY(0);
    opacity: 1;
  }
}
Enter fullscreen mode Exit fullscreen mode

Notifications.css:

/* src/pages/Notifications.css */
.notifications {
  max-width: 640px;
  margin: 0 auto;
  padding: 3rem 2rem 4rem;
}

.notifications h1 {
  font-size: 2rem;
  font-weight: 800;
  margin-bottom: 2rem;
}

.notification-list {
  list-style: none;
  display: flex;
  flex-direction: column;
  gap: 0;
}

.notification-item {
  display: flex;
  align-items: flex-start;
  gap: 1rem;
  padding: 1rem 0;
  border-bottom: 1px solid #f0f0f0;
}

.notification-item:last-child {
  border-bottom: none;
}

.notification-avatar {
  width: 36px;
  height: 36px;
  border-radius: 50%;
  background: #111;
  color: #fff;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 0.8rem;
  font-weight: 700;
  flex-shrink: 0;
}

.notification-body {
  display: flex;
  flex-direction: column;
  gap: 0.2rem;
}

.notification-body p {
  font-size: 0.9rem;
  color: #111;
  line-height: 1.5;
}

.notification-date {
  font-size: 0.8rem;
  color: #aaa;
}
Enter fullscreen mode Exit fullscreen mode

Congrats! You have completely built a React application with multiple pages. To view your app in the browser, type this command in your CLI:

npm run dev
Enter fullscreen mode Exit fullscreen mode

You should see a page like this:

demo app landing page

You are now ready to make your app multilingual.

How to Localize a Website

You’ve built a great website or web app with React, and now you want to make it available in multiple languages without significantly breaking your code.

In this section, you will learn how to localize your web app by using Lingo.dev, an AI tool that makes it super easy to translate React applications. Create a free account to get started.

Step 1: Install the Lingo.dev compiler

From the root directory of your app, type this command to install the Lingo.dev compiler:

pnpm install @lingo.dev/compiler
Enter fullscreen mode Exit fullscreen mode

Step 2: Update your app’s configuration for localization

Open your vite.config.ts file and add the lingoCompilerPlugin to your list of plugins above your react() plugin:

// vite.config.ts
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import { lingoCompilerPlugin } from "@lingo.dev/compiler/vite"; // import from Lingo.dev

export default defineConfig({
  plugins: [
  // lingoCompilerPlugin
    lingoCompilerPlugin({
      sourceRoot: "src",
      sourceLocale: "en",
      targetLocales: ["es", "de", "fr"],
      models: "lingo.dev",
      dev: {
        usePseudotranslator: false,
      },
    }),
    react(),
  ],
});
Enter fullscreen mode Exit fullscreen mode

The code above uses the lingoCompilerPlugin plugin to tell your application how it should handle localization. Inside it, you define:

  • sourceRoot: The directory where your app's source files live. The default value is src.
  • sourceLocale: The language your app is currently written in. This is set to English.
  • targetLocales: An array of the languages you want to translate your app into. Here, it is set to Spanish, German, and French. You can add more language codes if you want.
  • models: The translation engine to use. Use lingo.dev for easy setup.
  • usePseudotranslator: Set to false so you can get real translations instead of pseudo-translations.

NOTE: You must place the lingoCompilerPlugin before the react() plugin. This enables the compiler to transform your JSX before React processes it.

Step 3: Configure your API key

Create a .env file in your root folder to hold your Lingo.dev API key.

To get your API key, open your Lingo.dev dashboard, click the Get API key button at the top right, and copy your API key.

lingo.dev dashboard page

Now paste your API key into your .env file:

LINGODOTDEV_API_KEY=your_api_key
Enter fullscreen mode Exit fullscreen mode

Step 4: Connect your app to the translation compiler

Now, you need to connect your application to the translation compiler by wrapping
your app with LingoProvider in your entry point. Open your src/main.tsx file and add this:

import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import { BrowserRouter } from "react-router-dom";
import { LingoProvider } from "@lingo.dev/compiler/react"; //new 
import App from "./App";
import "./index.css";

createRoot(document.getElementById("root")!).render(
  <StrictMode>
    <LingoProvider>
      <BrowserRouter>
        <App />
      </BrowserRouter>
    </LingoProvider>
  </StrictMode>,
);
Enter fullscreen mode Exit fullscreen mode

The code above wraps the LingoProvider around your app.

Step 5: Add a language switcher

Next, add a language switcher to your app so users can easily switch between languages. Create a new file inside your components folder and call it LanguageSwitcher.tsx. Add this code to it:

import { useLingoContext } from "@lingo.dev/compiler/react";

export function LanguageSwitcher() {
  const { locale, setLocale } = useLingoContext();

  return (
    <div>
      <label>Language:</label>
      <select value={locale} onChange={(e) => setLocale(e.target.value)}>
        <option value="en">English</option>
        <option value="es">Español</option>
        <option value="de">Deutsch</option>
        <option value="fr">Français</option>
      </select>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Now update your navbar component to include the language switcher:

// src/components/Navbar.tsx
import { LanguageSwitcher } from "./LanguageSwitcher";

export function Navbar() {
  return (
    <nav className="navbar">
      <Link to="/" className="logo">
        MyApp
      </Link>
      <ul>
        <li>
          <Link to="/">Home</Link>
        </li>
        {/* other nav links */}
        </ul>
      <LanguageSwitcher />
    </nav>
  );
}
Enter fullscreen mode Exit fullscreen mode

Step 6: Test your translations

Congratulations! You have successfully localized your React application to four languages (English, Spanish, German, and French). To see this in action, open your CLI and run your development server:

npm run dev
Enter fullscreen mode Exit fullscreen mode

Next, open your app in the browser and try to toggle the language switcher:

landing page translated to German with a language switcher in the top right corner

The first time you do this, it will take a few seconds before the translated texts show up. After that, the translations will be stored in cache and only updated when you update your texts.

The Lingo.dev compiler will create a folder called lingo inside your src folder. You can find your cache here.

Step 7: Handle locale-based routing

Locale-based routing helps to boost your website's SEO by telling search engines which version of your page is intended for which audience. When a user in France searches for your app, Google can serve them /fr instead of /en.

To perform locale-based routing, do the following:

  1. Update your App.tsx file to prefix all routes with a locale segment:

    // src/App.tsx
    import { Routes, Route, Navigate } from "react-router-dom"; // updated import
    import { Navbar } from "./components/Navbar";
    import { Footer } from "./components/Footer";
    import { Home } from "./pages/Home";
    import { About } from "./pages/About";
    import { Contact } from "./pages/Contact";
    import { Notifications } from "./pages/Notifications";
    import "./App.css";
    
    export default function App() {
      // detect the browser's language
      const defaultLocale = navigator.language.split("-")[0] || "en";
      return (
        <div className="app">
          <Navbar />
          <main>
            <Routes>
              <Route
                path="/"
                element={<Navigate to={`/${defaultLocale}`} replace />}
              />{" "}
              {/* redirect to default locale */}
              <Route path="/:locale" element={<Home />} />
              <Route path="/:locale/about" element={<About />} />
              <Route path="/:locale/contact" element={<Contact />} />
              <Route path="/:locale/notifications" element={<Notifications />} />
            </Routes>
          </main>
          <Footer />
        </div>
      );
    }
    
  2. Update your language switcher to sync with the URL. This way, when a user switches the locale to a different language, the URL also changes to the right locale:

    // src/components/LanguageSwitcher.tsx
    import { useLingoContext } from "@lingo.dev/compiler/react";
    import { useNavigate, useLocation } from "react-router-dom";
    
    export function LanguageSwitcher() {
      const { locale, setLocale } = useLingoContext();
      const navigate = useNavigate();
      const location = useLocation();
    
      function handleChange(newLocale: string) {
        setLocale(newLocale);
        const rest = location.pathname.replace(/^\/[a-z]{2}/, "") || "/";
        navigate(`/${newLocale}${rest}`);
      }
      return (
        <div>
          <label>Language:</label>
          <select value={locale} onChange={(e) => handleChange(e.target.value)}>
            <option value="en">English</option>
            <option value="es">Español</option>
            <option value="de">Deutsch</option>
            <option value="fr">Français</option>
          </select>
        </div>
      );
    }
    

    The updated LanguageSwitcher component does two things at once when the user picks a language:

- It calls `setLocale` to update the active translation.
- It calls `navigate` to update the URL. The regex `/^\/[a-z]{2}/` strips the current locale prefix from the path so the new locale can be prepended cleanly.

Now, switching from `/fr/about` to Spanish gives you `/es/about`.
Enter fullscreen mode Exit fullscreen mode
  1. Finally, update your navbar links to use the active locale:

    // src/components/Navbar.tsx
    import { Link } from "react-router-dom";
    import { useLingoContext } from "@lingo.dev/compiler/react";
    import "./Navbar.css";
    import { LanguageSwitcher } from "./LanguageSwitcher";
    
    export function Navbar() {
      const { locale } = useLingoContext();
    
      return (
        <nav className="navbar">
          <Link to={`/${locale}`} className="logo">MyApp</Link>
          <ul>
            <li><Link to={`/${locale}`}>Home</Link></li>
            <li><Link to={`/${locale}/about`}>About</Link></li>
            <li><Link to={`/${locale}/contact`}>Contact</Link></li>
            <li><Link to={`/${locale}/notifications`}>Notifications</Link></li>
          </ul>
          <LanguageSwitcher />
        </nav>
      );
    }
    

    The navbar now reads the current locale from useLingoContext — the single source of truth for the active language. Every link is built with that locale, so navigating anywhere in the app keeps the URL and the translation in sync.

Now, you can restart your server by running:

npm run dev
Enter fullscreen mode Exit fullscreen mode

You should see the changes in your URL as you switch between pages and locales.

Step 8: Translate JSON in your app

If you observe your app, you will notice that the notifications on the notifications page are not translated. This is because the Lingo.dev compiler does not yet support translating JSON format. To fix this, you can use the Lingo.dev CLI.

notifications page with the title translated but not the notification messages themselves

Follow these steps to properly translate your notifications.

  1. Initialize the Lingo.dev CLI from your root directory:

    npx lingo.dev@latest init
    

    The command above will ask you a few questions, like:

- Your app’s primary language. Type “en” for this project,
- Your target languages. Type “es,de,fr” for this project,
- File format. Type “json” since you want to translate JSON format,
- If you want to  use (and create) the default path i18n/[locale].json. Type “y”.

You can skip any other questions it asks you or type “n”.

The command above will create an `i18n` folder and an `i18n.json` file in your root directory.
Enter fullscreen mode Exit fullscreen mode
  1. Update your i18n folder. This folder will contain an en.json file. Add your notifications object to this file as JSON:

    {
      "0": {
        "id": 1,
        "user": "Alice Martin",
        "action": "joined the team",
        "date": "2025-11-03"
      },
      "1": {
        "id": 2,
        "user": "Bob Chen",
        "action": "left a comment on your post",
        "date": "2025-11-05"
      }
    }
    
  2. Run this command from your root directory:

    npx lingo.dev@latest run
    

    This command will generate your translations. You should see the following files in your i18n folder:

- `es.json`
- `fr.json`
- `de.json`
Enter fullscreen mode Exit fullscreen mode
  1. Now update your Notifications.tsx file to use the right translation when languages get switched:

    // src/pages/Notifications.tsx
    import "./Notifications.css";
    import { useLingoContext } from "@lingo.dev/compiler/react";
    
    import en from "../../i18n/en.json";
    import es from "../../i18n/es.json";
    import de from "../../i18n/de.json";
    import fr from "../../i18n/fr.json";
    
    const translations = { en, es, de, fr };
    
    const dateFormatter = new Intl.DateTimeFormat(navigator.language, {
      year: "2-digit",
      month: "2-digit",
      day: "2-digit",
    });
    
    export function Notifications() {
      const { locale } = useLingoContext();
      const notifications = Object.values(translations[locale as keyof typeof translations],);
    
      return (
        <div className="notifications">
          <h1>Notifications</h1>
          <ul className="notification-list">
            {notifications.map((a) => (
              <li key={a.id} className="notification-item">
                <div className="notification-avatar">{a.user === "System" ? "S" : a.user[0]}</div>
                <div className="notification-body">
                  <p><strong>{a.user}</strong> {a.action}</p>
                  <span className="notification-date">{dateFormatter.format(new Date(a.date))}</span>
                </div>
              </li>
            ))}
          </ul>
        </div>
      );
    }
    

Now, if you run npm run dev and view your notifications page, you will get the right translations:

notifications page with proper translations for all translatable content

Step 9: Remove the Lingo.dev widget

Congratulations! Your app is almost ready to go live. The last thing you should do is to remove the Lingo.dev widget at the bottom left of your app:

contact page showing the contact form and a lingo.dev widget that persists across all pages

To do this, simply add the devWidget prop to your LingoProvider in the main.tsx file:

// src/main.tsx
<LingoProvider devWidget={{ enabled: false }}>
      <BrowserRouter>
        <App />
      </BrowserRouter>
    </LingoProvider>
Enter fullscreen mode Exit fullscreen mode

Now, the widget should disappear, and your app should be ready to go:

contact page displaying the contact form without the lingo.dev widget

Conclusion

Congratulations! You have successfully built a multilingual React application that speaks four languages. You can clone the project from the GitHub repository for reference.

As you have seen, localizing your React application does not require restructuring your code or changing how you write your components. The Lingo.dev Compiler handles everything at build time — every time you add new text, it gets picked up and translated automatically. Adding a new language is as simple as updating the targetLocales array in your config file.

When you are ready to take things further, you can integrate Lingo.dev into your CI/CD pipeline so translations are generated automatically on every deployment. You can also explore the Lingo.dev SDK for translating dynamic content like user-generated text that lives in your database rather than your code.

Top comments (0)