1
0 Comments

Multi-language with one codebase: 78 cards × 5 languages from one Lovable project

At Inithouse — a studio shipping a growing portfolio of products in parallel — we hit an interesting scaling problem early on: how do you serve five languages from a single codebase without turning your repo into a spaghetti pile of locale files?

Tarotas is a tarot self-reflection app with 78 cards, each with unique interpretations. When we decided to serve Czech, English, Polish, Slovak, and German from the same Lovable project and the same domain, the total content surface jumped to 390 card interpretations — plus UI strings, SEO meta, and per-locale landing copy.

Here's what we learned shipping it, and where the approach cracked.

The architecture: path-based locale switching

We went with URL-path locale routing: tarotas.com/cs/, tarotas.com/en/, tarotas.com/pl/. One React SPA, one Supabase backend, shared auth and analytics — the locale is just a route parameter that determines which content table to query.

The router itself is minimal:

const SUPPORTED_LOCALES = ['cs', 'en', 'pl', 'sk', 'de'] as const;
type Locale = typeof SUPPORTED_LOCALES[number];

function getLocaleFromPath(pathname: string): Locale {
  const segment = pathname.split('/')[1]?.toLowerCase();
  if (SUPPORTED_LOCALES.includes(segment as Locale)) {
    return segment as Locale;
  }
  const browserLang = navigator.language.slice(0, 2);
  return SUPPORTED_LOCALES.includes(browserLang as Locale)
    ? (browserLang as Locale)
    : 'en';
}

const { data: cards } = await supabase
  .from('card_interpretations')
  .select('*')
  .eq('locale', currentLocale);

This keeps the bundle small. One deploy serves all five languages. No separate builds, no parallel CI pipelines.

Where it worked well

Shared infrastructure costs. One Supabase project, one GA4 property, one Clarity dashboard. We tag by locale in analytics, but the operational overhead stays flat regardless of how many languages we add.

Fast launches. Adding Slovak took about two hours — the content structure already existed, we loaded translations into the same table, added 'sk' to the supported array, and deployed. No new domain registration, no DNS propagation wait, no separate hosting setup.

Cross-locale insights. Because everything lives in one analytics property, we can compare engagement across locales directly. We observed that Czech users explore more cards per session (avg ~4.2) than German users (~2.1) — which told us the German content needed more depth, not more traffic.

Where it cracked: translation drift

Tarot is not a 1:1 translation problem. The Czech "Smrt" card and the German "Tod" card share the same archetype, but the emotional weight differs across cultures. A direct translation of the interpretation text felt flat.

We ended up treating each locale's card content as a localized adaptation rather than a translation. This means the content tables diverged — they share structure but not text. The maintenance cost scales linearly with languages, and there is no shortcut around this for content-heavy products.

The hreflang trap: Czech vs Slovak

Google treats Czech and Slovak as closely related languages — which is accurate linguistically, but a headache for indexation. Without explicit hreflang alternate declarations, Google was indexing tarotas.com/cs/ and tarotas.com/sk/ as near-duplicates.

The fix was straightforward but easy to forget:

<link rel="alternate" hreflang="cs" href="https://tarotas.com/cs/" />
<link rel="alternate" hreflang="sk" href="https://tarotas.com/sk/" />
<link rel="alternate" hreflang="en" href="https://tarotas.com/en/" />
<link rel="alternate" hreflang="x-default" href="https://tarotas.com/en/" />

Every locale-specific page needs the full set. Miss one, and Google picks its own canonical — usually the wrong one.

The counter-example: domain-based multi-locale

Not every product in our portfolio uses this path-based approach. Živá Fotka — an AI photo-to-video tool — runs five separate domains: zivafotka.cz, zivafotka.sk, zywafotka.pl, alivephoto.online, lebendigfoto.de.

Why the difference? Živá Fotka targets local search intent in each market. Someone in Poland searches "ożyw zdjęcie" — they expect a Polish domain. The product name itself is localized (Živá Fotka → Żywa Fotka → Alive Photo → Lebendig Foto). Each domain builds its own domain authority in local Google results.

Path-based (Tarotas) works when:

  • The brand name travels across languages (Tarotas is Tarotas everywhere)
  • Content is the product (cards, interpretations — one domain concentrates authority)
  • Operational simplicity matters more than per-market SEO depth

Domain-based (Živá Fotka) works when:

  • The product name needs localization (language-native naming)
  • Local search intent dominates acquisition
  • Per-market landing pages need distinct messaging and pricing

We measured indexation patterns across both approaches. Tarotas consolidated its authority faster on a single domain — 78 pages indexed within three weeks for Czech. Živá Fotka's Polish domain took six weeks to reach comparable index coverage, but once indexed, it ranked for local queries that the .cz domain never touched.

When single-codebase multi-locale breaks down

The honest boundary: when each locale needs its own moderation rules, pricing tiers, or legal compliance stack. A tarot app does not have GDPR-distinct consent flows per country. A fintech product would.

For content-heavy consumer products in adjacent European markets, single-codebase path-based routing keeps the team small and the iteration fast. For products where the name, pricing, or legal context differs by market, separate domains earn their overhead.

At Inithouse, a studio running parallel product experiments, we now default to path-based for any product where the brand name is language-neutral, and domain-based when local naming drives acquisition. That split has saved us from both over-engineering and under-serving markets.


Building at Inithouse — try Tarotas in your language.

posted to Icon for group Building in Public
Building in Public
on June 19, 2026
Trending on Indie Hackers
Three Days Before Launch, I Let My Own Tool Tear Me Apart User Avatar 37 comments I thought I was building a news visualization tool. Users thought it was a catch-up tool. User Avatar 34 comments Priorities for launching a SaaS solo, with no budget User Avatar 31 comments I Rejected a $15K Acquisition Offer for My Multi-Agent IDE — Here's the Full Breakdown User Avatar 28 comments A pattern I keep seeing in EdTech: traffic isn't usually the problem. User Avatar 23 comments What Happens When a Photo Can Carry Multiple Voices? I Built VoxPho to Find Out User Avatar 15 comments