DEV Community

Cover image for How i built a Real-Time Multilingual Social Media Platform using Lingo.dev
T Narasimha
T Narasimha

Posted on

How i built a Real-Time Multilingual Social Media Platform using Lingo.dev

When we started building Nativly, we didn’t set out to “add translation.”

We were trying to solve something more fundamental:

Why should language decide who gets to participate in an online community?

Most community platforms assume English. If you don’t speak it comfortably, you adapt — or you leave.

We didn’t want adaptation.
We wanted inclusion by default.

This is the story of how we built a real-time multilingual community platform using:

  • Next.js
  • Supabase
  • Lingo.dev SDK And how we made translation feel invisible.

The Problem We Faced

Static UI translation is easy.

You use i18n.
You add JSON files.
You translate buttons.

But Nativly isn’t just UI.
It’s:

  • Posts
  • Comments
  • User bios
  • Community discussions All user-generated.

And here’s the real challenge:
If a Hindi user writes a post,

  • a Spanish user should read it in Spanish.
  • An English user should see it in English.
  • A Tamil user should see it in Tamil.

Same content Different viewer Real-time

Pre-translating every post into 20 languages? Not scalable.
Storing 20 columns per post? Messy.
Translating on the client? Unsafe.

We needed something smarter.

Our Core Idea: Store Once, Translate on View

Instead of storing translations in the database, we made a design decision that changed everything.

Store the original content only. Translate at render time based on the viewer.

Our Architecture (Simple but Powerful)

Here’s how Nativly works internally:

  1. User writes post in their native language.
  2. We store:
    • Original text
    • Source language
  3. When another user views the post:
    • We check their preferred language.
    • If it differs → we translate in real time using Lingo.dev.
  4. We cache the result.
  5. We render translated content.

That’s it.

No duplicated database rows.
No bloated schema.
No language-specific columns.

Just intelligent translation at the edge of interaction.

Our Database Design (Supabase)

Our posts table looks like this:

create table posts (
  id uuid primary key default uuid_generate_v4(),
  user_id uuid references profiles(id),
  original_text text not null,
  source_language varchar(10) not null,
  created_at timestamp with time zone default now()
);
Enter fullscreen mode Exit fullscreen mode

That’s all.

We don’t store text_hi, text_bn, text_ta.

Because translation is not storage.
It’s transformation.

Integrating Lingo.dev SDK (Server-Side)

We integrated Lingo.dev in our Next.js backend layer (Route Handlers).

We do not call it from the client. Why?

  • Keeps API key secure
  • Allows caching logic
  • Reduces abuse
  • Keeps translation consistent

Step 1: Installing SDK

npm install @lingo-dev/sdk
Enter fullscreen mode Exit fullscreen mode

Step 2: Creating a Translation Service /lib/lingo.ts

import Lingo from "@lingo-dev/sdk";

const lingo = new Lingo({
  apiKey: process.env.LINGO_API_KEY!,
});

export async function translateText(
  text: string,
  sourceLanguage: string,
  targetLanguage: string
) {
  if (sourceLanguage === targetLanguage) {
    return text;
  }

  const response = await lingo.translate({
    text,
    sourceLanguage,
    targetLanguage,
  });

  return response.translatedText;
}
Enter fullscreen mode Exit fullscreen mode

Simple.
No overengineering.

Real-Time Translation in Our API Route

When fetching posts for a user:
/app/api/feed/route.ts

import { translateText } from "@/lib/lingo";
import { createClient } from "@/lib/supabase/server";

export async function GET() {
  const supabase = createClient();

  const {
    data: { user },
  } = await supabase.auth.getUser();

  const { data: profile } = await supabase
    .from("profiles")
    .select("preferred_language")
    .eq("id", user?.id)
    .single();

  const viewerLanguage = profile?.preferred_language || "en";

  const { data: posts } = await supabase
    .from("posts")
    .select("*")
    .order("created_at", { ascending: false });

  const translatedPosts = await Promise.all(
    posts.map(async (post) => {
      const translated = await translateText(
        post.original_text,
        post.source_language,
        viewerLanguage
      );

      return {
        ...post,
        display_text: translated,
      };
    })
  );

  return Response.json(translatedPosts);
}
Enter fullscreen mode Exit fullscreen mode

Notice what we’re doing:

  • We don’t mutate stored content.
  • We compute display_text dynamically.
  • Translation depends entirely on who is viewing. That’s the key difference.

Making It Feel Instant (Caching Strategy)

Real-time translation is powerful.
But it can be expensive and slow if done naively.

So we added caching.

Our Strategy

If:

  • Post A (Hindi)
  • Viewed by 10 English users

We shouldn’t translate it 10 times.

We cache translations like this:

translation_cache:
post_id
target_language
translated_text
Enter fullscreen mode Exit fullscreen mode

Before calling Lingo.dev, we check:

const cached = await supabase
  .from("translation_cache")
  .select("translated_text")
  .eq("post_id", post.id)
  .eq("target_language", viewerLanguage)
  .single();
Enter fullscreen mode Exit fullscreen mode

If exists → return cached.
If not → call Lingo → store → return.

This reduced API calls drastically.

Why We Didn’t Pre-Translate

We tested pre-translation into 5 languages at post creation.

Problems:

  • Increased post creation latency.
  • Wasteful if no one reads in certain languages.
  • Hard to scale beyond fixed languages. Translation should happen when needed, not before.

UI Integration (Next.js)

Frontend is simple.
We just render:
<p>{post.display_text}</p>

Users never see:

  • loading spinners
  • raw language switches
  • language mismatch

It just works.
And that’s the goal.

Handling User Language Preferences

Our profiles table:

preferred_language varchar(10) default 'en'
Enter fullscreen mode Exit fullscreen mode

Users can select:

  • English
  • Hindi
  • Dutch
  • Japanese
  • French

Changing this setting instantly changes how the entire feed renders.

No reload hacks.
No separate routes.
Just dynamic translation.

What Lingo.dev made possible

Without Lingo:
We would need:

  • Manual translation pipelines
  • Separate language services
  • Heavy infra

Instead:
We get:

  • Clean SDK
  • Server-side integration
  • High-quality translations
  • Scalable API

It let us focus on product,
not language mechanics.

What We Learned

1.Translation is a Product Feature, Not a Utility

When users read content in their language:

  • They comment more.
  • They stay longer.
  • They engage deeper.

It changes their entire behavior.

2.Server-Side Translation is Cleaner

Doing translation inside API routes:

  • Keeps logic centralized
  • Avoids duplicated client calls
  • Makes caching easier
  • Protects your SDK key

3.Real-Time > Static for Communities

Static UI translation is necessary.
But it doesn’t solve community inclusion.

User-generated content must be dynamic.

That’s where Lingo.dev became essential for us.

Performance Considerations

We optimized:

  • Only translate when languages differ
  • Cache aggressively
  • Batch translation calls where possible
  • Use Promise.all for concurrency

Result:
Feed feels native.

Even when languages are mixed.

What Makes This Different

Many multilingual apps:

  • Translate UI
  • Ignore user content

Nativly:

  • Translates conversations.
  • Translates community interaction.
  • Makes cross-language dialogue natural.

It doesn’t just display languages.
It bridges them.

Where We’re Taking This Next

Future improvements:

  • Translation confidence scoring
  • Context-aware translation (community-specific tone)
  • Real-time comment streaming with translation
  • Language auto-detection for posts

But the foundation is stable.
And it’s simple.

Final Thoughts

We didn’t build translation because it was trendy.

We built it because communities shouldn’t fragment along language lines.

With:

  • Next.js for structure
  • Supabase for data
  • Lingo.dev for intelligent real-time translation We turned a local community idea into a borderless platform.

Same database.
Same post.
Different language.
Different experience.

That’s the power Lingo.dev brings to the table.

If this article helped you, share it with your friends or someone who's building cool stuff.

Top comments (0)