DEV Community

Mark Ndubuisi
Mark Ndubuisi

Posted on

Unison: Building a Workspace Where Language Barriers Don't Exist

The Problem Nobody Talks About

Remote work won. The debate is over. But here's the thing nobody seems to be solving well: your team is global now, but your tools still assume everyone speaks the same language.

Think about it. You've got a product manager in Tokyo writing specs in Japanese. A designer in Berlin annotating wireframes in German. A developer in Lagos writing task descriptions in English. They're all using the same Notion workspace, the same Slack channels, the same Google Docs — and they're all quietly struggling.

The usual workflow looks something like this: write something in your language, copy it, open Google Translate in another tab, paste, copy the result, paste it back into the shared doc. Multiply that by every message, every document, every comment, every day. It's exhausting, and it introduces a subtle but real friction that slows teams down and makes people feel like outsiders in their own workspace.

I had to work with some Japanese developers once, and I experienced this friction first-hand; I believe they did too. I had to manually translate messages sent to me before understanding what was sent, then I had to send back a manually-translated text, which was tiring and inefficient in a fast-paced modern workspace as the one we're in.

What We Built

Unison is a collaborative workspace where every piece of text — documents, chat messages, task descriptions, comments, UI labels — adapts to the reader's preferred language automatically. You write in yours. Your teammate reads in theirs. Nobody has to think about translation.

But Unison isn't just a "Google Docs with auto-translate bolted on." The core insight that drives the whole architecture is this: real-time collaboration and translation are fundamentally at odds, and you need a different model to make them work together.

The Features

Here's what Unison includes as a full workspace:

  • Collaborative Documents — A rich text editor with real-time sync, powered by Yjs CRDTs and TipTap
  • Chat Channels — Real-time messaging where every message is translated on-the-fly
  • Kanban Boards — Task management with drag-and-drop, priorities, and due dates
  • Whiteboards — An infinite canvas (powered by Tldraw) for visual collaboration
  • Git-Inspired Document Branching — The piece I'm most proud of, and the one that required the most thinking

All of it, in 12 languages. All of it, translated in real-time.

The Hard Problem: Why "Just Translate Everything" Doesn't Work

Early on, I tried the obvious approach: everyone edits the same document simultaneously (standard CRDT behavior), and we just translate the content for each viewer.

It broke immediately.

When two people type into the same paragraph at the same time — one in English, one in Japanese — the CRDT merges their keystrokes character by character. You end up with gibberish: half-English, half-Japanese fragments that can't be translated because they're not valid text in either language.

The core tension is: CRDTs are designed to merge concurrent edits at the character level. Translation operates on complete sentences and paragraphs. These two models are incompatible when applied to the same shared state.

The Solution: Git-Inspired Branching

The answer came from an unlikely place: Git.

In Git, you don't have everyone pushing to main simultaneously. You branch, you work, you submit a pull request, someone reviews, and then your changes get merged. We applied the same model to document collaboration:

  1. The document owner works on main — they edit the canonical version directly
  2. Collaborators get personal branches — when you open a shared document, Unison automatically creates your own branch (a fork of main). You edit freely in your own language, on your own Y.Doc instance
  3. Submit for review — when you're done, you submit your branch. The owner sees it in their Merge Panel
  4. Translate and merge — Unison translates the branch content into the document's original language, the owner reviews the translated preview, and merges it into main

Each branch gets its own Yjs document, its own Supabase Realtime channel (yjs:{docId}:branch:{branchId}), and its own persistence. Branches are fully isolated — no character-level conflicts across languages.

Owner (English)     ──── edits main Y.Doc ────────────────> main
                                                              ↑
Collaborator A (Japanese) ──── edits branch A ── submit ──── merge (translate JP → EN)
                                                              ↑
Collaborator B (Spanish)  ──── edits branch B ── submit ──── merge (translate ES → EN)
Enter fullscreen mode Exit fullscreen mode

This was the "aha" moment. By separating the editing contexts and deferring the merge to a deliberate review step, we avoid the CRDT-vs-translation conflict entirely. Each person edits a clean, single-language document. Translation only happens once, at merge time, on complete content.

The Translation Architecture

Translation is the backbone of Unison, so getting it right was critical. We built a two-tier system:

Tier 1: Static UI Strings (Zero API Calls)

Every button, label, tooltip, and placeholder in the app is pre-translated into all 12 supported languages and bundled at build time. A useUITranslation() hook reads the user's preferred language from the Zustand store and returns the right string instantly — no network request, no loading state.

const { t } = useUITranslation();
// t("sidebar.documents") → "Documents" | "ドキュメント" | "Documentos" | ...
Enter fullscreen mode Exit fullscreen mode

This covers 100+ UI string keys across every component. When you switch your language, the entire interface updates instantly.

Tier 2: Dynamic Content Translation (Three-Layer Cache)

For user-generated content — document text, chat messages, task titles, comments — we use a useTranslation() hook backed by a three-layer cache:

  1. L1: In-memory Map — instant for repeated translations within a session
  2. L2: Supabase translation_cache table — persists across sessions, shared across users
  3. L3: Translation API — calls Lingo.dev SDK

The result: the first time someone's Japanese message gets translated to English, it hits the API. Every subsequent view — by any user, in any session — is a cache hit. Translation costs amortize to near-zero over time.

Lingo.dev as the Translation Engine

We use Lingo.dev as our primary translation engine via their SDK. It handles the actual text localization:

const engine = new LingoDotDevEngine({ apiKey: process.env.LINGODOTDEV_API_KEY });
const result = await engine.localizeText(text, {
  sourceLocale: fromLanguage,
  targetLocale: toLanguage,
});
Enter fullscreen mode Exit fullscreen mode

Real-Time Chat Translation

The chat system is where translation feels most magical. Here's the flow:

  1. A user in Tokyo types a message in Japanese and hits Enter
  2. The message is stored in Supabase with original_language: "ja"
  3. Supabase Realtime broadcasts the INSERT to all channel subscribers
  4. Each recipient's client receives the message and passes it through useTranslation()
  5. A user in Berlin sees the message in German. A user in Lagos sees it in English.

The sender sees their original message. The "Translated from Japanese" badge is subtle — just enough context to know it's a translation, not enough to break the flow of conversation.

The Tech Stack

Layer Technology Why
Framework Next.js 16 (App Router) Server components for initial data loading, client components for interactivity
Database + Auth + Realtime Supabase PostgreSQL with RLS for security, Realtime for live updates, Auth for identity — one service for three concerns
Real-Time Collaboration Yjs + TipTap Yjs CRDTs handle conflict-free editing; TipTap gives us a rich text editor on top
Translation Lingo.dev SDK + DeepL (fallback) Reliable, high-quality translations with redundancy
Whiteboard Tldraw Mature infinite-canvas library, stores state as serializable snapshots
State Management Zustand Lightweight, no boilerplate, perfect for user/workspace context
Drag & Drop @hello-pangea/dnd Accessible, performant drag-and-drop for the Kanban board
Styling Custom CSS + CSS variables Theming (dark/light mode), animations, no utility-class bloat in the markup

Architecture Decisions I'd Make Again

Branch-per-user instead of shared editing for cross-language docs. This was the biggest departure from "how collaboration tools usually work" and it paid off. The tradeoff is that collaborators can't see each other's edits in real-time — but that's actually fine when they're writing in different languages. You wouldn't be able to read their edits anyway.

Pre-translated UI strings instead of runtime translation. Translating 100+ UI strings at runtime would mean either (a) 100+ API calls on page load or (b) a loading spinner while UI text loads. Neither is acceptable. Pre-translating everything into a static dictionary and bundling it means the UI is always instant, regardless of language.

Three-layer translation cache. The in-memory cache makes repeated renders free. The database cache means translations survive page refreshes and are shared across users. The API is only called for genuinely new content.

Supabase for everything. Using one service for auth, database, and realtime means fewer integration points, fewer credentials, fewer things to debug at 2am during a hackathon. The RLS policies also mean our security model is enforced at the database level, not just in application code.

What I Learned

  • Building multilingual applications is a complex task, I never realized it until I started.
  • The modern workspace needs more tools like Unison, and we need to build them.
  • Building the collaborative document editor was probably the hardest of all, because I had to make it seamless and friction-less for collaborators.
  • If I had to start over, I will go with the diffing approach first, and then iterate from their; I believe I will have a better result that way.

What's Next

  • With guidance from the lingo.dev team, I plan to improve the translation mode from whole-text translation to per-word translation with backwards translation feature.
  • I plan to improve on the diffing mechanism to allow a smoother flow for collaborators on documents.
  • I will also improve the document editor tools, for more rich text editing.
  • I plan to integrate voice translation too (optimistic feature), so that teams can send voice notes across to each other, and also use the speech-to-text feature in editing documents.

Unison was built for the lingo.dev hackathon. The idea is simple: your tools should adapt to you, not the other way around. Language is context, not a barrier.

GitHub repo
Live demo


devtochukwu is product-oriented software engineer, with years of experience building real-world solutions on different platforms. Find him on X and LinkedIn.

Top comments (0)