DEV Community

Cover image for Building In-Context i18n Editing: A Developer's Guide to Seamless Translation Management
Gaurav9695328446
Gaurav9695328446

Posted on

Building In-Context i18n Editing: A Developer's Guide to Seamless Translation Management

🎯 Introduction

Imagine this: Your content team needs to update a translation. Instead of navigating through a complex CMS, digging through JSON files, or waiting for a developer to make changes, they simply click on the text they want to edit—right there on the page—make their changes, and save. That's the power of in-context i18n editing.

In this article, we'll build a production-ready in-context internationalization system using React and Next.js. You'll learn how to create an intuitive editing experience that empowers non-technical team members while maintaining clean, maintainable code.


🤔 The Problem

Traditional i18n workflows often involve:

  • Developers managing translation files
  • Content teams working in separate CMS systems
  • Disconnect between what users see and what gets translated
  • Time-consuming back-and-forth between teams
  • Risk of breaking the UI with incorrect translations

✨ The Solution

In-context editing bridges this gap by allowing translations to be edited directly where they appear, providing:

Benefit Impact
Visual context Editors see exactly where translations appear
Immediate feedback Changes are visible instantly
Reduced friction No need to switch between systems
Better accuracy Context-aware translations

📚 What is In-Context i18n?

In-context internationalization (i18n) editing is a development pattern that allows content editors to modify translations directly within the application interface. Instead of editing JSON files or using a separate CMS, editors can:

  1. Click on any translatable text
  2. Edit it inline
  3. Save changes that persist to your backend
  4. See updates reflected immediately

Perfect For:

  • 🛍️ Marketing teams updating landing page copy
  • 📝 Content managers maintaining multi-language websites
  • 🎨 Product teams iterating on UI text
  • 🧪 QA teams fixing translation errors during testing

🏗️ Architecture Overview

Our in-context i18n system follows a clean, modular architecture:

┌─────────────────────────────────────────────────────────┐
│                     React App                            │
│  ┌───────────────────────────────────────────────────┐  │
│  │           TranslationProvider                      │  │
│  │  ┌─────────────────────────────────────────────┐  │  │
│  │  │         Context API                         │  │  │
│  │  │  ┌──────────────┐    ┌──────────────────┐  │  │  │
│  │  │  │ EditableText │    │ LanguageSwitcher │  │  │  │
│  │  │  └──────────────┘    └──────────────────┘  │  │  │
│  │  └─────────────────────────────────────────────┘  │  │
│  └───────────────────────────────────────────────────┘  │
└─────────────────────────────────────────────────────────┘
                         ↓ ↑
                    Backend API
                         ↓ ↑
                 Translation Database
Enter fullscreen mode Exit fullscreen mode

Key Components

  1. TranslationProvider: Manages translation state and API communication
  2. EditableText: Wraps translatable content with editing capabilities
  3. LanguageSwitcher: Allows switching between languages
  4. Backend API: Handles CRUD operations for translations

Data Flow

User clicks text → EditableText enters edit mode → 
User edits → Save → API call → Backend updates → 
State refresh → UI updates
Enter fullscreen mode Exit fullscreen mode

🚀 Implementation Steps

Let's build this step by step. We'll use Next.js 16 with the App Router and React 19.

Step 1: Setting Up the Project

First, create a new Next.js project:

npx create-next-app@latest incontext-i18n-frontend
cd incontext-i18n-frontend
npm install
Enter fullscreen mode Exit fullscreen mode

Step 2: Creating the Translation Provider

The TranslationProvider is the heart of our system. It manages current language state, translation data, API communication, and edit mode.

Create i18n/TranslationProvider.js:

"use client";
import { createContext, useEffect, useState } from "react";

const TranslationContext = createContext();

export function TranslationProvider({ children }) {
  const [lang, setLang] = useState("en");
  const [translations, setTranslations] = useState({});
  const [editMode] = useState(process.env.NODE_ENV === "development");

  useEffect(() => {
    fetch(`http://localhost:4000/api/translations/${lang}`)
      .then((res) => res.json())
      .then(setTranslations);
  }, [lang]);

  const t = (key) => translations[key] || key;

  const updateTranslation = async (key, value) => {
    await fetch(`http://localhost:4000/api/translations/${key}/${lang}`, {
      method: "PUT",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ value }),
    });

    setTranslations((prev) => ({ ...prev, [key]: value }));
  };

  return (
    <TranslationContext.Provider
      value={{ lang, setLang, t, editMode, updateTranslation, translations }}
    >
      {children}
    </TranslationContext.Provider>
  );
}

export { TranslationContext };
Enter fullscreen mode Exit fullscreen mode

💡 Key Features:

  • Fetches translations when language changes
  • Provides t() function for translation lookup
  • Handles translation updates via API
  • Edit mode only enabled in development

Step 3: Creating the useTranslation Hook

A custom hook provides clean access to translation context:

"use client";

import { useContext } from "react";
import { TranslationContext } from "./TranslationProvider";

export function useTranslation() {
  const context = useContext(TranslationContext);

  if (!context) {
    throw new Error("useTranslation must be used within a TranslationProvider");
  }

  return context;
}
Enter fullscreen mode Exit fullscreen mode

Step 4: Building the EditableText Component

This is where the magic happens! ✨ The EditableText component displays translated text normally, switches to edit mode on click (in development), provides inline editing with keyboard shortcuts, and saves changes automatically.

Create components/EditableText.js:

"use client";

import { useState, useEffect } from "react";
import { useTranslation } from "../i18n/useTranslation";

export function EditableText({ translationKey, i18nKey, className = "" }) {
  const { t, updateTranslation, editMode, lang, translations } = useTranslation();
  const key = i18nKey || translationKey;
  const [isEditing, setIsEditing] = useState(false);
  const [value, setValue] = useState(t(key));

  useEffect(() => {
    if (!isEditing) {
      const currentTranslation = translations[key] || key;
      setTimeout(() => setValue(currentTranslation), 0);
    }
  }, [lang, translations, key, isEditing]);

  const handleEdit = () => {
    if (editMode) {
      setValue(t(key));
      setIsEditing(true);
    }
  };

  const handleSave = () => {
    updateTranslation(key, value);
    setIsEditing(false);
  };

  const handleCancel = () => {
    setValue(t(key));
    setIsEditing(false);
  };

  if (isEditing && editMode) {
    return (
      <input
        type="text"
        value={value}
        onChange={(e) => setValue(e.target.value)}
        onBlur={handleSave}
        onKeyDown={(e) => {
          if (e.key === "Enter") handleSave();
          else if (e.key === "Escape") handleCancel();
        }}
        autoFocus
        className="glass-input"
        style={{
          position: "relative",
          zIndex: 1000,
          padding: "8px 12px",
          border: "2px solid rgba(255, 255, 255, 0.5)",
          borderRadius: "8px",
          backgroundColor: "rgba(255, 255, 255, 0.25)",
          backdropFilter: "blur(10px)",
          color: "white",
          outline: "none",
          minWidth: "200px",
          fontSize: "inherit",
          fontWeight: "inherit",
          boxShadow: "0 4px 12px rgba(0, 0, 0, 0.2)",
        }}
      />
    );
  }

  return (
    <span
      className={`editable-text ${className}`}
      onClick={handleEdit}
      style={{ cursor: editMode ? "pointer" : "default" }}
    >
      {t(key)}
    </span>
  );
}
Enter fullscreen mode Exit fullscreen mode

🎮 User Experience Features:

  • Click to edit
  • Enter to save
  • Escape to cancel
  • Auto-focus on edit mode
  • Blur to save (click outside)

Step 5: Creating the Language Switcher

A simple component to switch between languages:

"use client";

import { useTranslation } from "../i18n/useTranslation";

export default function LanguageSwitcher() {
  const { lang, setLang } = useTranslation();

  return (
    <select
      value={lang}
      onChange={(e) => setLang(e.target.value)}
      className="glass-input"
      style={{
        padding: "8px 16px",
        fontSize: "14px",
        fontWeight: "600",
        borderRadius: "8px",
        cursor: "pointer",
        appearance: "none",
        backgroundImage: `url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='white' d='M6 9L1 4h10z'/%3E%3C/svg%3E")`,
        backgroundRepeat: "no-repeat",
        backgroundPosition: "right 12px center",
        paddingRight: "36px",
      }}
    >
      <option value="en">English</option>
      <option value="fr">French</option>
      <option value="ja">Japanese</option>
    </select>
  );
}
Enter fullscreen mode Exit fullscreen mode

Step 6: Styling with Glassmorphism

We'll use a modern glassmorphism design. Add to app/globals.css:

body {
  background: linear-gradient(
    135deg,
    #667eea 0%,
    #764ba2 25%,
    #f093fb 50%,
    #4facfe 75%,
    #00f2fe 100%
  );
  background-size: 400% 400%;
  animation: gradientShift 15s ease infinite;
  min-height: 100vh;
}

@keyframes gradientShift {
  0% { background-position: 0% 50%; }
  50% { background-position: 100% 50%; }
  100% { background-position: 0% 50%; }
}

.glass-card {
  background: rgba(255, 255, 255, 0.1);
  backdrop-filter: blur(20px);
  border: 1px solid rgba(255, 255, 255, 0.2);
  border-radius: 20px;
  box-shadow: 
    0 8px 32px 0 rgba(31, 38, 135, 0.37),
    inset 0 1px 0 0 rgba(255, 255, 255, 0.3);
}

.glass-input {
  background: rgba(255, 255, 255, 0.15);
  backdrop-filter: blur(10px);
  border: 1px solid rgba(255, 255, 255, 0.3);
  border-radius: 12px;
  color: white;
}

.glass-input:focus {
  outline: none;
  border-color: rgba(255, 255, 255, 0.5);
  background: rgba(255, 255, 255, 0.2);
  box-shadow: 0 0 0 3px rgba(255, 255, 255, 0.1);
}
Enter fullscreen mode Exit fullscreen mode

Step 7: Using EditableText in Your Components

Now you can use EditableText anywhere in your app:

import { EditableText } from "../components/EditableText";
import LanguageSwitcher from "../components/LanguageSwitcher";

export default function Home() {
  return (
    <main>
      <h1>
        <EditableText i18nKey="home.title" />
      </h1>
      <LanguageSwitcher />

      <label>
        <EditableText i18nKey="login.username.label" />
      </label>
      <input placeholder="Enter your username" />

      <button>
        <EditableText i18nKey="login.submit" />
      </button>
    </main>
  );
}
Enter fullscreen mode Exit fullscreen mode

🔍 Code Deep Dive

How Translation Lookup Works

The t() function provides a fallback mechanism:

const t = (key) => translations[key] || key;
Enter fullscreen mode Exit fullscreen mode
  • ✅ If translation exists: returns translated value
  • ⚠️ If missing: returns the key itself (prevents blank text)
  • 🚀 In production: shows the key as fallback
  • 🛠️ In development: you can edit to add the translation

State Management Strategy

We use React Context for global state because:

  • Simple: No need for Redux or complex state management
  • Lightweight: Perfect for translation data
  • Reactive: Components update automatically when language changes
  • Scalable: Easy to add features like caching or optimistic updates

Edit Mode Logic

Edit mode is controlled by environment:

const [editMode] = useState(process.env.NODE_ENV === "development");
Enter fullscreen mode Exit fullscreen mode

Why this approach?

  • ✅ Production safety: No accidental edits in live sites
  • 🛠️ Development convenience: Immediate editing during development
  • 🔧 Alternative: Use feature flags or user roles for production editing

🎨 Key Features

1. Inline Editing

Click any translatable text to edit it directly. No modals, no separate pages—just click and type.

2. Keyboard Shortcuts

  • Enter: Save changes
  • Escape: Cancel editing
  • Blur: Auto-save on click outside

3. Language Switching

Switch languages instantly with the dropdown. All translations update automatically.

4. Visual Feedback

  • Cursor changes to pointer in edit mode
  • Input field appears with glassmorphism styling
  • Smooth transitions between states

5. Development-Only Editing

Edit mode is automatically disabled in production, preventing accidental changes.

6. Fallback Handling

Missing translations show the key instead of blank text, making it easy to identify what needs translation.


📖 Best Practices

1. Organize Translation Keys

Use a hierarchical naming convention:

// ✅ Good
"home.title"
"login.username.label"
"checkout.payment.methods.creditCard"

// ❌ Avoid
"title1"
"label2"
"text3"
Enter fullscreen mode Exit fullscreen mode

2. Handle Loading States

Add loading indicators while fetching translations:

const [loading, setLoading] = useState(true);

useEffect(() => {
  setLoading(true);
  fetch(`/api/translations/${lang}`)
    .then((res) => res.json())
    .then((data) => {
      setTranslations(data);
      setLoading(false);
    });
}, [lang]);
Enter fullscreen mode Exit fullscreen mode

3. Error Handling

Implement proper error handling:

const updateTranslation = async (key, value) => {
  try {
    const response = await fetch(`/api/translations/${key}/${lang}`, {
      method: "PUT",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ value }),
    });

    if (!response.ok) {
      throw new Error("Failed to update translation");
    }

    setTranslations((prev) => ({ ...prev, [key]: value }));
  } catch (error) {
    console.error("Translation update failed:", error);
    // Show error toast or revert changes
  }
};
Enter fullscreen mode Exit fullscreen mode

4. Cache Translations

Store translations in localStorage for offline support:

useEffect(() => {
  const cached = localStorage.getItem(`translations-${lang}`);
  if (cached) {
    setTranslations(JSON.parse(cached));
  }

  fetch(`/api/translations/${lang}`)
    .then((res) => res.json())
    .then((data) => {
      setTranslations(data);
      localStorage.setItem(`translations-${lang}`, JSON.stringify(data));
    });
}, [lang]);
Enter fullscreen mode Exit fullscreen mode

🖥️ Backend API Requirements

Your backend should provide these endpoints:

GET /api/translations/:lang

Returns all translations for a language:

{
  "home.title": "Welcome",
  "login.username.label": "Username",
  "login.submit": "Sign In"
}
Enter fullscreen mode Exit fullscreen mode

PUT /api/translations/:key/:lang

Updates a specific translation:

Request:

{
  "value": "New Translation Text"
}
Enter fullscreen mode Exit fullscreen mode

Response:

{
  "success": true,
  "key": "home.title",
  "lang": "en",
  "value": "New Translation Text"
}
Enter fullscreen mode Exit fullscreen mode

Example Backend (Node.js/Express)

const express = require("express");
const app = express();

app.use(express.json());

// In-memory store (use a database in production)
const translations = {
  en: {
    "home.title": "Welcome",
    "login.username.label": "Username",
    "login.submit": "Sign In"
  },
  fr: {
    "home.title": "Bienvenue",
    "login.username.label": "Nom d'utilisateur",
    "login.submit": "Se connecter"
  }
};

app.get("/api/translations/:lang", (req, res) => {
  const { lang } = req.params;
  res.json(translations[lang] || {});
});

app.put("/api/translations/:key/:lang", (req, res) => {
  const { key, lang } = req.params;
  const { value } = req.body;

  if (!translations[lang]) {
    translations[lang] = {};
  }

  translations[lang][key] = value;
  res.json({ success: true, key, lang, value });
});

app.listen(4000, () => {
  console.log("Translation API running on http://localhost:4000");
});
Enter fullscreen mode Exit fullscreen mode

🚀 Advanced Enhancements

1. Translation History

Track changes over time to see who edited what and when.

2. Translation Status

Show which translations are missing or need review with visual indicators.

3. Bulk Editing

Allow editing multiple translations at once for faster workflows.

4. Translation Suggestions

Integrate with translation APIs for AI-powered suggestions.


🎯 Conclusion

In-context i18n editing transforms how teams manage translations. By allowing direct editing within the application, you:

  • Reduce friction between content and development teams
  • 🚀 Speed up iteration on translations
  • 🎯 Improve accuracy with visual context
  • 💪 Empower non-technical team members

Next Steps

  1. Add error handling and loading states
  2. Implement caching for better performance
  3. Add user permissions for production editing
  4. Integrate with your CMS or translation service
  5. Add analytics to track translation usage

📚 Resources


Happy coding! 🚀

If you found this article helpful, consider:

  • ⭐ Starring the repository
  • 📝 Sharing your own implementation
  • 💬 Leaving feedback or questions in the comments
  • 🔄 Following for more React/Next.js tutorials

What's your approach to managing translations? Have you tried in-context editing? Share your thoughts in the comments below! 👇

Top comments (0)