<link rel="stylesheet" href="/assets/fonts/jetbrains-mono/jetbrains-mono.css" />
All posts

A blog in 7 languages with translated URLs: how this site is built

This blog started in Italian. When I added the other six languages — English, Albanian, Portuguese, Spanish, French and German — translating the text was the easy part. The hard part was everything else: URLs, links already shared, search engines and a machine translation that seemed to work but was silently breaking the content.

The problem: one Italian URL for seven languages

At first each article had a single slug, the Italian one. The result was addresses like /de/blog/perche-il-blog-non-viene-indicizzato: a German page with an Italian URL. Ugly to share, unclear to readers and a waste for SEO, because the words in a URL matter.

The data model: one article, seven languages

I chose to keep a single document per article, with per-language fields: title_en, content_de, slug_fr and so on. The alternative was one document per translation, but then I'd have had to keep tags, views and publication status in sync across seven copies. With one document, the article stays one thing.

Translated slugs that never change

Each per-language slug is generated only once, from the translated title, and never touched again — not even if I fix the title: a URL that has been shared must keep working. On top of that, every slug must be unique across all fields of all articles, so an address always leads to exactly one article, whatever language it comes from:

// Fill slug_xx from the translated title, only where it's missing.
// Existing slugs are never touched: they're in URLs people have shared.
for (const lang of ['en', 'sq', 'pt', 'es', 'fr', 'de']) {
  if (post[`slug_${lang}`]) continue;
  const title = post[`title_${lang}`];
  if (title?.trim()) {
    post[`slug_${lang}`] = await ensureUniqueSlug(title); // unique across ALL slug fields
  }
}

Old links keep working

Before translated slugs, many links had already been shared in the form /sq/blog/italian-slug. Breaking them would have meant 404 pages and negative signals for Google. That's why the build also generates every article with the Italian slug under each language prefix, with the canonical pointing to the translated version: people arriving from the old link see the right page, and Google knows which address is the official one.

SEO for every language

  • hreflang on every page, linking together the seven versions of the same article.
  • Dynamic og:locale, so social previews show the correct language, title and description.
  • A sitemap with every version, updated automatically on every publication.
  • Prerendered pages per language: without them Google only saw the application shell, as I described in this article.

The machine translation that broke the code

To translate without touching the HTML, I replaced tags and code blocks with placeholders, sent the text to the translator and then put everything back. It worked almost always. But when several placeholders sat close together — typical of articles with many code blocks — the translator sometimes duplicated one and dropped another. It wasn't random: re-running it produced the exact same error, especially in Albanian.

The fix was to stop sending placeholders to the translator at all: I split the text on the placeholders and translate only the prose pieces. More calls, but no chance of a placeholder being altered:

// Split on the placeholders: the translator never sees them
const parts = text.split(/(✦\d+✦)/);
const out = [];
for (const part of parts) {
  out.push(/^✦\d+✦$/.test(part) ? part : await translate(part, lang));
}
return out.join('');

Since then, after every translation I compare each language's structure with the Italian one — same number of headings, paragraphs, tables and code blocks — without trusting the "no errors" reported by the script.


In short

A multilingual blog isn't a blog with a translator bolted on: you need a clear data model, stable URLs for every language, old links that don't break and automatic checks on translation. They're the same choices I propose when a client wants a site in several languages. If that's your case, get in touch.

💬 Reader notes

0 notes

Write a note

Share your opinion, a suggestion or a compliment

Latest notes

No notes yet. Be the first to comment!