Multilingual Portfolio: Reaching a Global Audience
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;
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
Projects
canalizações de dados
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