The Pirson Dev Portfolio uses i18next and react-i18next for internationalization, supporting multiple languages with automatic browser language detection.
i18next Configuration
The i18n setup is initialized in src/main.jsx before the React app renders:
import i18next from 'i18next' ;
import { I18nextProvider } from "react-i18next" ;
import global_es from "./locales/es/global.json" ;
import global_en from "./locales/en/global.json" ;
import global_fr from "./locales/fr/global.json" ;
// Detect browser language (first 2 characters)
const browserLang = navigator . language . slice ( 0 , 2 );
// Retrieve last selected language from localStorage
const savedLang = localStorage . getItem ( "lang" );
let defaultLang ;
if ( savedLang ) {
defaultLang = savedLang ;
} else if ( browserLang === "es" || browserLang === "en" || browserLang === "fr" ) {
defaultLang = browserLang ;
} else {
defaultLang = "es" ;
}
i18next . init ({
interpolation: { escapeValue: false },
lng: defaultLang ,
resources: {
es: { global: global_es },
en: { global: global_en },
fr: { global: global_fr },
}
})
// Save language changes to localStorage
i18next . on ( "languageChanged" , ( lng ) => {
localStorage . setItem ( "lang" , lng );
});
Key Features
Browser Language Detection : Automatically detects user’s browser language
Persistent Selection : Saves user’s language choice in localStorage
Fallback Language : Defaults to Spanish (“es”) if browser language isn’t supported
Translation File Structure
Translation files are organized in src/locales/{language}/global.json:
src/
└── locales/
├── en/
│ └── global.json
├── es/
│ └── global.json
└── fr/
└── global.json
Example Translation File
src/locales/en/global.json
{
"app" : {
"name" : "Pirson " ,
"animated_titles" : [ "Dev" , "Programmer" , "Developer" , "Full Stack" ]
},
"navbar" : {
"home" : "Home" ,
"about" : "About Me" ,
"projects" : "Projects" ,
"contact" : "Contact"
},
"home" : {
"title" : "Francisco Cortés Pirson" ,
"subtitle" : "Full Stack Web Developer" ,
"description" : "Always exploring new technologies and best practices to turn ideas into complete, functional, and high-performance web solutions." ,
"copied" : "Copied!" ,
"social" : {
"github" : "GitHub" ,
"linkedin" : "LinkedIn" ,
"email" : "Copy Email" ,
"whatsapp" : "WhatsApp" ,
"cv" : "Download CV"
}
},
"about" : {
"title" : "About Me" ,
"description_1" : "I'm a junior web developer in constant learning, comfortable working in teams and taking on challenges that foster both personal and professional growth." ,
"description_2" : "I stand out for my leadership, adaptability, and ability to solve problems creatively and efficiently." ,
"sections" : {
"education" : "Education" ,
"certificates" : "Certificates" ,
"skills" : "Skills"
},
"button_certificates_more" : "Show more" ,
"button_certificates_less" : "Show less"
},
"routes" : {
"about-me" : "/about-me" ,
"projects" : "/projects" ,
"contact" : "/contact"
}
}
Using Translations in Components
Import and use the useTranslation hook from react-i18next:
import { useTranslation } from "react-i18next" ;
const Home = () => {
const [ t ] = useTranslation ( "global" );
return (
< div >
< h1 > { t ( "home.title" ) } </ h1 >
< h2 > { t ( "home.subtitle" ) } </ h2 >
< p > { t ( "home.description" ) } </ p >
</ div >
);
};
Accessing Nested Properties
Use dot notation to access nested translation keys:
{ t ( "about.sections.education" )}
{ t ( "home.social.github" )}
{ t ( "navbar.projects" )}
Using Arrays
Access array values with returnObjects: true:
< LayoutTextFlip
text = { t ( "app.name" ) }
words = { t ( "app.animated_titles" , { returnObjects: true }) }
/>
Dynamic Routes
Translate route paths for localized URLs:
import { useTranslation } from "react-i18next" ;
function AppContent () {
const [ t ] = useTranslation ( "global" );
return (
< Routes >
< Route path = "/" element = { < Home /> } />
< Route path = { t ( "routes.about-me" ) } element = { < About /> } />
< Route path = { t ( "routes.projects" ) } element = { < Projects /> } />
< Route path = { t ( "routes.contact" ) } element = { < Contact /> } />
</ Routes >
);
}
This creates localized routes:
English : /about-me, /projects, /contact
Spanish : /sobre-mi, /proyectos, /contacto
French : /a-propos, /projets, /contact
Adding a New Language
Create Translation File
Create a new folder and JSON file in src/locales/: mkdir src/locales/de
touch src/locales/de/global.json
Copy and Translate Content
Copy the structure from an existing language file: src/locales/de/global.json
{
"app" : {
"name" : "Pirson " ,
"animated_titles" : [ "Dev" , "Programmierer" , "Entwickler" , "Full Stack" ]
},
"navbar" : {
"home" : "Startseite" ,
"about" : "Über mich" ,
"projects" : "Projekte" ,
"contact" : "Kontakt"
},
"home" : {
"title" : "Francisco Cortés Pirson" ,
"subtitle" : "Full Stack Webentwickler" ,
"description" : "Immer auf der Suche nach neuen Technologien und Best Practices, um Ideen in vollständige, funktionale und leistungsstarke Weblösungen zu verwandeln." ,
"copied" : "Kopiert!"
}
// ... rest of translations
}
Import in main.jsx
Import the new translation file: import global_de from "./locales/de/global.json" ;
Add to i18next Resources
Register the new language: i18next . init ({
interpolation: { escapeValue: false },
lng: defaultLang ,
resources: {
es: { global: global_es },
en: { global: global_en },
fr: { global: global_fr },
de: { global: global_de }, // Add new language
}
})
Update Language Detection
Add the new language code to the detection logic: if ( savedLang ) {
defaultLang = savedLang ;
} else if ( browserLang === "es" || browserLang === "en" || browserLang === "fr" || browserLang === "de" ) {
defaultLang = browserLang ;
} else {
defaultLang = "es" ;
}
Update Language Switcher
Add the new language option to your language switcher component (if you have one).
Content with Embedded Data
Some sections like education, certificates, and skills include both translatable text and static data:
src/locales/en/global.json
{
"about" : {
"education" : [
{
"year" : "2025" ,
"title" : "Full Stack Java, Spring Boot, React and SQL Bootcamp" ,
"school" : "Esplai Formación" ,
"src" : "/assets/img/logo-FE.png" ,
"alt" : "Logo Esplai"
}
],
"certificates" : [
{
"title" : "Microsoft Certified Azure AI Fundamentals" ,
"school" : "Microsoft" ,
"date" : "December 2025" ,
"src" : "/assets/certificados/img/certificado_azure_ia_fundamentals.jpg"
}
],
"skills" : [
{
"category" : "Frontend" ,
"subcategories" : [
{
"title" : "Languages" ,
"items" : [
{ "name" : "HTML" , "src" : "/assets/svg/html.svg" },
{ "name" : "CSS" , "src" : "/assets/svg/css.svg" }
]
}
]
}
]
}
}
When translating these sections, keep the src, alt, and asset paths the same across all languages. Only translate the text content like title, school, date, and category.
Best Practices
Maintain the same key structure across all language files. If you add a new key in English, add it to all other languages: // All language files should have the same keys
{
"home" : {
"new_feature" : "Translation text"
}
}
Always provide a fallback language (usually English) for missing translations: i18next . init ({
fallbackLng: 'en' ,
// ... other config
})
Make translation keys descriptive and organized by section: {
"about" : {
"sections" : {
"education" : "Education" ,
"certificates" : "Certificates"
}
}
}
Avoid generic keys like text1, label2, etc.
Keep Formatting Consistent
Switching Languages Programmatically
Change the current language using i18next.changeLanguage():
import { useTranslation } from "react-i18next" ;
const LanguageSwitcher = () => {
const { i18n } = useTranslation ();
const changeLanguage = ( lng ) => {
i18n . changeLanguage ( lng );
};
return (
< div >
< button onClick = { () => changeLanguage ( 'en' ) } > English </ button >
< button onClick = { () => changeLanguage ( 'es' ) } > Español </ button >
< button onClick = { () => changeLanguage ( 'fr' ) } > Français </ button >
</ div >
);
};
Next Steps
Content Learn how to update personal information and content
Styling Customize colors, fonts, and visual styling