Overview
Portfolio Moretto uses react-i18next for internationalization, making it easy to support multiple languages. The portfolio comes with English and Spanish translations out of the box.
Translation File Structure
All translations are stored as JSON files in the dictionaries folder:
source/src/components/dictionaries/
├── en.json # English translations
└── es.json # Spanish translations
Example Translation File
Here’s a portion of the English translation file:
{
"header.home" : "Home" ,
"header.about" : "About" ,
"header.technology" : "Skills" ,
"header.works" : "Experience" ,
"header.projects" : "Projects" ,
"header.contact" : "Contact" ,
"hero.tagline" : "Software Developer" ,
"hero.title" : "I turn ideas into software that truly works" ,
"hero.description" : "I design, build, and maintain digital products that blend simple interfaces with solid, long-lasting architecture." ,
"hero.highlights" : [
{ "label" : "Years in tech" , "value" : "2+" },
{ "label" : "Projects delivered" , "value" : "5+" },
{ "label" : "Based in" , "value" : "Argentina" }
],
"about.subtitle" : "Story" ,
"about.title" : "From tech support to full-stack developer"
}
And the corresponding Spanish version:
{
"header.home" : "Inicio" ,
"header.about" : "Sobre mí" ,
"header.technology" : "Skills" ,
"header.works" : "Experiencia" ,
"header.projects" : "Proyectos" ,
"header.contact" : "Contacto" ,
"hero.tagline" : "Software Developer" ,
"hero.title" : "Transformo ideas en software que realmente funciona" ,
"hero.description" : "Diseño, desarrollo y mantengo productos digitales que combinan interfaces simples con una arquitectura sólida y pensada para durar." ,
"hero.highlights" : [
{ "label" : "Años en tecnología" , "value" : "2+" },
{ "label" : "Proyectos entregados" , "value" : "5+" },
{ "label" : "Desde" , "value" : "Argentina" }
],
"about.subtitle" : "Historia" ,
"about.title" : "De soporte técnico a desarrollador full-stack"
}
Notice how both files have identical keys but different values. This is crucial for the translation system to work correctly.
i18n Configuration
The translation system is configured in src/i18n.js:
import i18n from 'i18next' ;
import { initReactI18next } from 'react-i18next' ;
import translationES from './components/dictionaries/es.json'
import translationEN from './components/dictionaries/en.json'
const resources = {
es: {
translation: translationES
},
en: {
translation: translationEN
}
}
i18n
. use ( initReactI18next )
. init ({
resources ,
lng: 'es' , // Default language
debug: true ,
keySeparator: false ,
interpolation: {
escapeValue: false ,
}
})
export default i18n ;
Configuration Options Explained
Object containing all available languages and their translation files.
The default language when the app loads. Currently set to 'es' (Spanish).
When true, logs helpful information to the console during development.
Set to false because we use dot notation in our keys (e.g., "hero.title").
interpolation.escapeValue
Set to false because React already escapes values to prevent XSS attacks.
Using the useTranslation Hook
The useTranslation hook is used throughout the codebase to access translations.
Basic Usage
import { useTranslation } from "react-i18next"
function MyComponent () {
const { t } = useTranslation ()
return (
< div >
< h1 > { t ( "hero.title" ) } </ h1 >
< p > { t ( "hero.description" ) } </ p >
</ div >
)
}
Retrieving Arrays and Objects
When your translation key contains an array or object, use returnObjects: true:
import { useTranslation } from "react-i18next"
function Hero () {
const { t } = useTranslation ()
const highlights = t ( "hero.highlights" , { returnObjects: true })
return (
< ul >
{ Array . isArray ( highlights ) &&
highlights . map (( item , index ) => (
< li key = { `hero-highlight- ${ index } ` } >
< p > { item ?. value } </ p >
< p > { item ?. label } </ p >
</ li >
)) }
</ ul >
)
}
Always check if the result is an array using Array.isArray() before mapping to prevent runtime errors.
Language-Specific Logic
Sometimes you need different behavior based on the current language:
import { useTranslation } from "react-i18next"
function Project ({ project }) {
const { t , i18n } = useTranslation ()
const description =
i18n . language === "es"
? project ?. descriptionSpanish ?? project ?. description ?? ""
: project ?. descriptionEnglish ?? project ?. description ?? ""
return < p > { description } </ p >
}
This pattern is used in the Project component to display the correct description based on the selected language.
Changing Languages
The language switcher in the Header component demonstrates how to toggle between languages:
import { useTranslation } from "react-i18next"
import { useState } from "react"
function Header () {
const { t , i18n } = useTranslation ()
const toggleLanguage = () => {
const nextLanguage = i18n . language === "es" ? "en" : "es"
i18n . changeLanguage ( nextLanguage )
}
return (
< button
type = "button"
onClick = { toggleLanguage }
className = "rounded-full border border-slate-700 bg-slate-900/70 px-3 py-1"
>
{ t ( "changeLanguage" ) }
</ button >
)
}
The changeLanguage text itself is also translatable:
// en.json
"changeLanguage" : "ES / EN"
// es.json
"changeLanguage" : "EN / ES"
Adding a New Language
Follow these steps to add a new language (e.g., French):
Create the translation file
Create a new file fr.json in src/components/dictionaries/: {
"header.home" : "Accueil" ,
"header.about" : "À propos" ,
"header.technology" : "Compétences" ,
"header.works" : "Expérience" ,
"header.projects" : "Projets" ,
"header.contact" : "Contact" ,
"hero.tagline" : "Développeur logiciel" ,
"hero.title" : "Je transforme les idées en logiciels qui fonctionnent vraiment" ,
"hero.description" : "Je conçois, développe et maintiens des produits numériques..."
}
Import and register the language
Update src/i18n.js to include the new language: import i18n from 'i18next' ;
import { initReactI18next } from 'react-i18next' ;
import translationES from './components/dictionaries/es.json'
import translationEN from './components/dictionaries/en.json'
import translationFR from './components/dictionaries/fr.json' // Add this
const resources = {
es: {
translation: translationES
},
en: {
translation: translationEN
},
fr: { // Add this
translation: translationFR
}
}
i18n
. use ( initReactI18next )
. init ({
resources ,
lng: 'es' ,
// ... rest of config
})
Update the language switcher
Modify the language toggle logic to support more than two languages: const toggleLanguage = () => {
const languages = [ 'es' , 'en' , 'fr' ]
const currentIndex = languages . indexOf ( i18n . language )
const nextIndex = ( currentIndex + 1 ) % languages . length
i18n . changeLanguage ( languages [ nextIndex ])
}
Update the language button text
// en.json
"changeLanguage" : "ES / FR"
// es.json
"changeLanguage" : "EN / FR"
// fr.json
"changeLanguage" : "EN / ES"
Copy one of the existing language files (like en.json) and use it as a template to ensure you don’t miss any keys.
Adding New Translation Keys
When you need to add new content to your portfolio:
Choose a semantic key name
Use dot notation to organize keys logically:
section.subsection.property
Example: testimonials.title or certifications.items
Add the key to ALL language files
{
"testimonials.title" : "What people say" ,
"testimonials.subtitle" : "Testimonials"
}
{
"testimonials.title" : "Lo que dicen sobre mí" ,
"testimonials.subtitle" : "Testimonios"
}
Use the translation in your component
import { useTranslation } from "react-i18next"
function Testimonials () {
const { t } = useTranslation ()
return (
< section >
< span > { t ( "testimonials.subtitle" ) } </ span >
< h2 > { t ( "testimonials.title" ) } </ h2 >
</ section >
)
}
Complex Data Structures
The translation system supports nested objects and arrays.
Arrays of Strings
"about.description" : [
"Hi! I'm Federico Moretto — a developer passionate about..." ,
"At Asince SRL, I combine incident response with product development..." ,
"My time working in foreign trade strengthened my analytical thinking..."
]
Usage:
const description = t ( "about.description" , { returnObjects: true })
{ Array . isArray ( description ) &&
description . map (( paragraph , index ) => (
< p key = { `about-paragraph- ${ index } ` } > { paragraph } </ p >
))}
Arrays of Objects
"skills.groups" : [
{
"title" : "Front-end" ,
"items" : [ "HTML" , "CSS" , "JavaScript" , "TypeScript" , "React" ]
},
{
"title" : "Back-end" ,
"items" : [ "C# .NET" , "Java" , "Python" , "Node.js" ]
}
]
Usage:
const groups = t ( "skills.groups" , { returnObjects: true })
{ Array . isArray ( groups ) &&
groups . map (( group , index ) => (
< div key = { `skills-group- ${ index } ` } >
< h3 > { group ?. title } </ h3 >
< ul >
{ Array . isArray ( group ?. items ) &&
group . items . map (( item , itemIndex ) => (
< li key = { `skills- ${ index } - ${ itemIndex } ` } >
{ item }
</ li >
)) }
</ ul >
</ div >
))}
Deeply Nested Objects
"works.items" : [
{
"role" : "Technical Support & Software Developer" ,
"company" : "Asince SRL" ,
"period" : "2022 — Present" ,
"summary" : "I keep the company's core platform stable..." ,
"achievements" : [
"Handled and resolved technical support tickets." ,
"Developed and implemented hotfixes for production environments."
],
"technologies" : [ "C# .NET" , "Vue" , "TypeScript" , "Azure" ]
}
]
Translation Best Practices
Maintain key consistency Every language file must have the exact same keys, even if some values are similar across languages.
Use semantic naming Name keys based on their purpose, not their content. Use hero.callToAction instead of hero.button1.
Keep similar lengths Try to keep translations roughly the same length to avoid layout issues.
Test all languages Switch between languages regularly during development to catch missing or incorrect translations.
Use pluralization sparingly If you need complex pluralization, consider using i18next’s pluralization features.
Avoid hardcoded text All user-facing text should go through the translation system, even if you only support one language.
Common Patterns
Conditional Content
const works = t ( "works.items" , { returnObjects: true })
{ Array . isArray ( works ) &&
works . map (( work , index ) => (
< article key = { `work- ${ index } ` } >
{ work ?. summary && < p > { work . summary } </ p > }
{ Array . isArray ( work ?. achievements ) && work . achievements . length > 0 && (
< ul >
{ work . achievements . map (( achievement , i ) => (
< li key = { i } > { achievement } </ li >
)) }
</ ul >
) }
</ article >
))}
Fallback Values
const description =
i18n . language === "es"
? project ?. descriptionSpanish ?? project ?. description ?? ""
: project ?. descriptionEnglish ?? project ?. description ?? ""
Dynamic Keys
const typeLabels = {
sitioWeb: t ( "projects.typeLabels.website" ),
varios: t ( "projects.typeLabels.misc" ),
}
const displayType = typeLabels [ project ?. type ] || project ?. type
Debugging Translations
Check the console
With debug: true in i18n.js, missing translations will be logged to the console.
Verify key names
Make sure the key you’re using in your component exactly matches the key in the JSON file (including dots).
Check returnObjects
If you’re accessing an array or object, make sure you’re using { returnObjects: true }.
Validate JSON syntax
Use a JSON validator to ensure your translation files are properly formatted.
Default Language
To change the default language, update the lng property in i18n.js:
i18n
. use ( initReactI18next )
. init ({
resources ,
lng: 'en' , // Changed from 'es' to 'en'
// ...
})
Persisting Language Preference
Currently, the selected language is not persisted. To save the user’s preference:
// In Header.jsx or i18n.js
const toggleLanguage = () => {
const nextLanguage = i18n . language === "es" ? "en" : "es"
i18n . changeLanguage ( nextLanguage )
localStorage . setItem ( 'preferredLanguage' , nextLanguage ) // Save preference
}
// In i18n.js initialization
const savedLanguage = localStorage . getItem ( 'preferredLanguage' )
i18n . init ({
resources ,
lng: savedLanguage || 'es' , // Use saved language or default
// ...
})
Translation Resources
react-i18next Documentation Official documentation for the react-i18next library
i18next Documentation Core i18next library documentation with advanced features
Next Steps
Styling Customization Learn how to customize the visual appearance with Tailwind CSS