Multilingual Portfolio: Reaching a Global Audience

Role: Full-Stack DeveloperDec 2025

A portfolio that speaks six languages, where the hard part was deciding what not to translate.


The decision that shaped everything

A Brazilian developer reading about "data pipelines" expects to see data pipelines, not canalizações de dados. The English term is the industry's. Translating it reads as awkward at best and condescending at worst.

That one example set the rule for the whole system: the work is not translating a portfolio into six languages. It is deciding, term by term, what should survive untranslated — and then making that decision reproducible.

What the system does

The site runs on next-intl with localePrefix: 'always', so every route is nested under a locale segment. i18n/config.ts defines six:

export const locales = ['en', 'pt-BR', 'es', 'ja', 'fr', 'de'] as const;
English Source
Write once in English
DeepL or Claude
Translation or transcreation
Localization Rules
Apply tech term rules
Locale Files
Six locale versions

Content lives as locale-suffixed files. hello-world.mdx is the English base; hello-world.pt-BR.mdx and hello-world.ja.mdx sit beside it. lib/content.ts looks for the requested locale's file and falls back to the English base when it is absent.

Sorting metadata always comes from the base file. Date, pinned and lastUpdated are read from the English source regardless of the locale being rendered, so a post cannot occupy a different position in the Japanese list than in the English one.

UI strings fall back by deep merge. i18n/request.ts loads messages/en.json as a base and merges the locale catalog over it, key by key. A missing key renders the English string rather than a MISSING_MESSAGE error, and a half-finished catalog is safe to ship.

The fallback is labelled, not silent

An earlier version of this page claimed the English fallback happened silently. That was the wrong goal, and it is not what the site does.

When a page falls back, getContentLocaleStatus() returns 'fallback', and two things follow: the content element is marked lang="en" so screen readers and search engines are told which language they are actually getting, and a visible translation disclaimer appears above the content.

Quietly serving English under a /ja/ URL would mislead both the reader and the crawler. Saying so costs one line of UI.

Two pipelines, different jobs

DeepL handles direct translation — fast, consistent, and prone to over-translating technical vocabulary. A Claude agent handles transcreation: it adapts idioms, reorders sentences for natural flow, and keeps tech terms in English per the rules below.

npm run translate      # DeepL: messages/*.json
npm run translate:mdx  # DeepL: MDX content
npm run i18n:mdx       # Claude: transcreation
npm run i18n:sync      # Claude: fill missing translations

Both write locale-suffixed files beside the English source. Neither is trusted blindly — output gets read back before it ships, checking that frontmatter keys stay in English, image paths and URLs are untouched, code blocks and tables survive intact, and className values are not translated.

Localization rules in practice

Always translate: navigation and UI labels, article content, form fields, calls to action.

Always keep in English: product names (Next.js, TypeScript, Claude), company names (Nike, Meta), and the technical vocabulary each audience already uses in English — API, dashboard, data pipeline, template.

Per locale:

  • pt-BR — "dashboard" not "painel", "template" not "modelo", "e-commerce" not "comércio eletrônico"
  • es — "dashboard" not "panel de control", "marketers" not "vendedores"
  • ja — product names in Latin script, not katakana; katakana for common loanwords
  • fr — "dashboard" not "tableau de bord"; natural CTAs ("Contactez-moi")
  • de — informal "du", English loanwords for tech terms, nouns capitalized
How 'Projects' appears in each language

Projects

ENLiteral translation

canalizações de dados

PT-BRLocalized (preserved)

data pipelines

What I'd tell myself at the start

Localization is a product decision wearing a technical costume. The pipeline was the easy part; the rules about what to leave alone were the work, and they are the part that has to be written down, because they are the part a translation tool will cheerfully get wrong every single time.

The measure of success is not that the site has six languages. It is that a reader in São Paulo or Tokyo finds the vocabulary they would have used themselves.

A note on translations

Translations here are machine-generated, then reviewed against the rules above. Automated translation is a starting point, not a finished product.

If something reads awkwardly in your language, I'd welcome the correction through the contact form. Native speakers catch what automation misses.

Outcomes

  • Six locales: English, Portuguese (Brazil), Spanish, Japanese, French, and German
  • Two pipelines: DeepL for direct translation, a Claude agent for transcreation
  • Per-locale URLs with hreflang alternates for each language
  • Missing translations fall back to English, labelled rather than hidden
  • Localization rules keep the technical terms each audience expects in English