Overview
The portfolio implements a complete internationalization system supporting Portuguese (ptBR) and English (en) with persistent language selection, client-side state management, and centralized content storage.
Architecture
Zustand Store Client-side state management with localStorage persistence
Content JSON Centralized bilingual content structure
Translation Utility Type-safe translation helper function
Language Store (Zustand)
The language state is managed with Zustand for simplicity and performance:
Store Location
src/store/language-store.ts
Store Implementation
import { create } from "zustand" ;
export type Lang = "ptBR" | "en" ;
type LanguageState = {
lang : Lang ;
hydrated : boolean ;
hydrate : () => void ;
setLang : ( lang : Lang ) => void ;
};
const KEY = "portfolio-language" ;
export const useLanguageStore = create < LanguageState >(( set , get ) => ({
lang: "ptBR" ,
hydrated: false ,
hydrate : () => {
if ( get (). hydrated ) return ;
const stored = ( localStorage . getItem ( KEY ) as Lang | null ) ?? "ptBR" ;
set ({ lang: stored , hydrated: true });
},
setLang : ( lang ) => {
localStorage . setItem ( KEY , lang );
set ({ lang });
}
}));
The hydrate function is called once on app mount to sync state with localStorage, preventing hydration mismatches in Next.js.
Hydration Hook
To prevent hydration errors in Next.js, a custom hook initializes the store:
Hook Location
src/app/hooks/use-language-hydrate.ts
Implementation
import { useLanguageStore } from "@/store/language-store" ;
import { useEffect } from "react" ;
export function useLanguageHydrate () {
const hydrate = useLanguageStore (( s ) => s . hydrate );
useEffect (() => hydrate (), [ hydrate ]);
}
Usage in Layout
// src/app/layout.tsx
import { useLanguageHydrate } from "@/app/hooks/use-language-hydrate" ;
export default function RootLayout ({ children }) {
useLanguageHydrate (); // Call once at app root
return (
< html >
< body > { children } </ body >
</ html >
);
}
The hydration pattern ensures the server-rendered HTML matches the initial client render, preventing React hydration warnings.
Translation Utility
A simple translation helper handles both string and object formats:
Utility Location
Implementation
type Lang = "ptBR" | "en" ;
type I18nText = string | { ptBR : string ; en : string };
export function translate ( value : I18nText , lang : Lang ) {
if ( typeof value === "string" ) return value ;
return value [ lang ] ?? value . ptBR ;
}
Usage Examples
import { translate } from "@/utils/i18n" ;
import { useLanguageStore } from "@/store/language-store" ;
import content from "@/utils/content.json" ;
function MyComponent () {
const { lang } = useLanguageStore ();
return (
< div >
{ /* String literal (no translation needed) */ }
< h1 > { translate ( content . hero . name , lang ) } </ h1 >
{ /* Returns: "Thalyson Rafael" */ }
{ /* Bilingual object */ }
< p > { translate ( content . hero . tagline , lang ) } </ p >
{ /* Returns (ptBR): "Transformo visão técnica..." */ }
{ /* Returns (en): "I turn technical vision..." */ }
</ div >
);
}
Content Structure
All translatable content is stored in a single JSON file:
Content Location
Structure Pattern
Content follows consistent patterns for translation:
Simple String (No Translation)
Bilingual Object
Nested Bilingual Content
Arrays with Bilingual Items
{
"hero" : {
"name" : "Thalyson Rafael"
}
}
Language Switcher Component
The language switcher is a Select component:
Component Location
src/components/select-language.tsx
Implementation
import {
Select ,
SelectContent ,
SelectItem ,
SelectTrigger ,
SelectValue
} from "@/components/ui/select" ;
import { Lang , useLanguageStore } from "@/store/language-store" ;
export function SelectLanguage () {
const { lang , setLang } = useLanguageStore ();
return (
< Select value = { lang } onValueChange = { ( value ) => setLang ( value as Lang ) } >
< SelectTrigger className = "max-w-36 w-fit min-h-12 border-l-0 border-b-0" >
< SelectValue placeholder = "PT-BR" />
</ SelectTrigger >
< SelectContent >
< SelectItem value = "ptBR" > PT-BR </ SelectItem >
< SelectItem value = "en" > EN </ SelectItem >
</ SelectContent >
</ Select >
);
}
The language selection is automatically persisted to localStorage via the Zustand store’s setLang function.
Adding New Languages
Update Type Definition
Add new language code to the Lang type: // src/store/language-store.ts
export type Lang = "ptBR" | "en" | "es" ; // Added Spanish
Update Translation Utility
Modify the translate function to handle the new language: // src/utils/i18n.ts
type Lang = "ptBR" | "en" | "es" ;
type I18nText = string | { ptBR : string ; en : string ; es ?: string };
export function translate ( value : I18nText , lang : Lang ) {
if ( typeof value === "string" ) return value ;
return value [ lang ] ?? value . en ?? value . ptBR ; // Fallback chain
}
Add Translations to Content
Update content.json with new language keys: {
"hero" : {
"title" : {
"ptBR" : "Desenvolvedor Full Stack" ,
"en" : "Full Stack Developer" ,
"es" : "Desarrollador Full Stack"
}
}
}
Update Language Switcher
Add new option to SelectLanguage component: < SelectContent >
< SelectItem value = "ptBR" > PT-BR </ SelectItem >
< SelectItem value = "en" > EN </ SelectItem >
< SelectItem value = "es" > ES </ SelectItem >
</ SelectContent >
Adding New Translations
Identify Content Location
Determine which section of content.json needs the new content: {
"hero" : { /* Hero section content */ },
"about" : { /* About section content */ },
"projects" : { /* Projects section content */ },
"contact" : { /* Contact section content */ }
}
Add Bilingual Entry
Add your new content with both language versions: {
"hero" : {
"newField" : {
"ptBR" : "Novo conteúdo em português" ,
"en" : "New content in English"
}
}
}
Use in Component
Import and use the translation: import { translate } from "@/utils/i18n" ;
import { useLanguageStore } from "@/store/language-store" ;
import content from "@/utils/content.json" ;
function MyComponent () {
const { lang } = useLanguageStore ();
return (
< p > { translate ( content . hero . newField , lang ) } </ p >
);
}
Best Practices
Always add translations to content.json rather than hardcoding strings in components. This:
Makes it easier to find and update translations
Ensures consistency across the app
Allows for easy export to translation services
Keeps components clean and focused on logic
Structure your content.json with clear, semantic keys: ✅ Good
{
"contact" : {
"form" : {
"submitButton" : { "ptBR" : "Enviar" , "en" : "Submit" }
}
}
}
❌ Bad
{
"text1" : { "ptBR" : "Enviar" , "en" : "Submit" }
}
The translate function already provides a fallback to ptBR. Ensure all content has at least the default language: // Will fallback to ptBR if 'en' is missing
return value [ lang ] ?? value . ptBR ;
Some words translate differently based on context. Use descriptive keys: {
"projects" : {
"viewButton" : { "ptBR" : "Ver projeto" , "en" : "View project" },
"viewAllLink" : { "ptBR" : "Ver todos" , "en" : "View all" }
}
}
Type Safety
For enhanced type safety, consider generating TypeScript types from your content.json: // scripts/generate-i18n-types.ts
import content from '../src/utils/content.json' ;
import fs from 'fs' ;
type DeepKeys < T > = T extends object
? { [ K in keyof T ] : ` ${ K & string }${ DeepKeys < T [ K ]> extends never ? '' : `. ${ DeepKeys < T [ K ]> } ` } ` }[ keyof T ]
: never ;
type ContentKeys = DeepKeys < typeof content >;
const types = `
export type Lang = 'ptBR' | 'en';
export type ContentKey = ${ JSON . stringify ( content , null , 2 ) } ;
` ;
fs . writeFileSync ( 'src/types/i18n.ts' , types );
This prevents typos when accessing content keys.
The i18n system is optimized for performance:
Client-side only : Translation happens on the client, avoiding server-side overhead
Single JSON import : All content is bundled once, not split by language
Zustand optimization : Selective re-renders using Zustand’s selector pattern
localStorage caching : Language preference persists across sessions
For even better performance, consider:
Code-splitting content.json by section
Using dynamic imports for large translation objects
Implementing a translation management service for production
Debugging Tips
Use React DevTools or add a debug component: function LanguageDebug () {
const { lang , hydrated } = useLanguageStore ();
return (
< div className = "fixed bottom-4 right-4 bg-black text-white p-2 text-xs" >
Lang: { lang } | Hydrated: { hydrated ? '✓' : '✗' }
</ div >
);
}
Check localStorage in browser DevTools: // Console
localStorage . getItem ( 'portfolio-language' )
// Clear to reset
localStorage . removeItem ( 'portfolio-language' )
Missing Translation Warnings
Add a development-only warning: export function translate ( value : I18nText , lang : Lang ) {
if ( typeof value === "string" ) return value ;
if ( process . env . NODE_ENV === 'development' && ! value [ lang ]) {
console . warn ( `Missing translation for language: ${ lang } ` , value );
}
return value [ lang ] ?? value . ptBR ;
}