The 200 Mates application supports three languages: Spanish (ES), English (EN), and Portuguese (PT). This page documents the i18n implementation and how to add or modify translations.
Language Support
Spanish Primary language (default)
English International audience
Portuguese Brazilian users
Translation System Architecture
Core Components
Translation Object (data/i18n.js) - All translations
Translation Function (modules/utils.js) - Lookup function
DOM Attributes (index.html) - Marked elements
Language Switcher (app.js) - User controls
Data Structure
const i18n = {
es: {
title: "Vuelta al Mundo en unos 200 Mates" ,
statMates: "Mates" ,
statCountries: "Países" ,
formTitle: "Cebate uno" ,
placeholderName: "Nombre*" ,
placeholderCountry: "País*" ,
placeholderYerba: "Marca de yerba*" ,
bitter: "Amargo" ,
sweet: "Dulce" ,
terere: "Tereré" ,
brewedMateTea: "Mate Cocido" ,
submitBtn: "Enviar" ,
alertRequired: "Por favor completá los campos obligatorios y adjuntá una foto." ,
// ... 100+ keys
},
en: {
title: "Around the World in about 200 Mates" ,
statMates: "Mates" ,
statCountries: "Countries" ,
formTitle: "Share yours" ,
placeholderName: "Name*" ,
placeholderCountry: "Country*" ,
placeholderYerba: "Yerba brand*" ,
bitter: "Bitter" ,
sweet: "Sweet" ,
terere: "Tereré" ,
brewedMateTea: "Brewed Mate Tea" ,
submitBtn: "Submit" ,
alertRequired: "Please fill in all required fields and attach a photo." ,
// ...
},
pt: {
title: "Volta ao Mundo em cerca de 200 Mates" ,
statMates: "Mates" ,
statCountries: "Países" ,
formTitle: "Compartilhe o seu" ,
placeholderName: "Nome*" ,
placeholderCountry: "País*" ,
placeholderYerba: "Marca de erva*" ,
bitter: "Amargo" ,
sweet: "Doce" ,
terere: "Tereré" ,
brewedMateTea: "Mate Cozido" ,
submitBtn: "Enviar" ,
alertRequired: "Por favor, preencha todos os campos obrigatórios e anexe uma foto." ,
// ...
}
};
Translation Function
t(key)
Retrieves a translated string for the current language:
function t ( key ) {
return window . i18n ?.[ currentLang ]?.[ key ] ?? // Try current language
window . i18n ?. es ?.[ key ] ?? // Fallback to Spanish
key ; // Fallback to key itself
}
Parameters:
key (string): Translation key from the i18n object
Returns : Translated string
Example Usage:
t ( "title" ) // Spanish: "Vuelta al Mundo en unos 200 Mates"
t ( "submitBtn" ) // Spanish: "Enviar"
t ( "nonexistentKey" ) // Returns: "nonexistentKey"
The function uses optional chaining (?.) and nullish coalescing (??) for safe fallback behavior:
Try current language
Fallback to Spanish (default)
Return the key itself if no translation exists
DOM Integration
data-i18n Attribute
Marks elements for automatic translation:
< h1 data-i18n = "title" ></ h1 >
When applyI18n() is called, this becomes:
< h1 data-i18n = "title" > Vuelta al Mundo en unos 200 Mates </ h1 >
data-i18n-placeholder Attribute
Translates placeholder attributes:
< input type = "text" id = "name" data-i18n-placeholder = "placeholderName" placeholder = "" >
Becomes:
< input type = "text" id = "name" data-i18n-placeholder = "placeholderName" placeholder = "Nombre*" >
Applying Translations
applyI18n()
Updates all translatable elements in the DOM:
function applyI18n () {
// Update innerHTML for data-i18n elements
document . querySelectorAll ( "[data-i18n]" ). forEach ( el => {
const v = t ( el . dataset . i18n );
if ( v && v !== el . dataset . i18n ) {
el . innerHTML = v ;
}
});
// Update placeholder attributes
document . querySelectorAll ( "[data-i18n-placeholder]" ). forEach ( el => {
const v = t ( el . dataset . i18nPlaceholder );
if ( v ) {
el . placeholder = v ;
}
});
// Re-render gallery with translated text
renderGallery ();
}
When Called:
On page load (app.js)
When user switches language
After any language state change
Using innerHTML for translations can introduce XSS vulnerabilities if translation strings contain user input. The 200 Mates translations are static and safe, but be cautious when adding dynamic translations.
Language Switcher
HTML Structure
< div class = "lang-switcher" >
< button class = "lang-btn active" data-lang = "es" > ES </ button >
< span class = "lang-sep" > · </ span >
< button class = "lang-btn" data-lang = "en" > EN </ button >
< span class = "lang-sep" > · </ span >
< button class = "lang-btn" data-lang = "pt" > PT </ button >
</ div >
JavaScript Handler
document . querySelectorAll ( ".lang-btn" ). forEach ( btn => {
btn . addEventListener ( "click" , () => {
currentLang = btn . dataset . lang ;
// Update active state
document . querySelectorAll ( ".lang-btn" ). forEach ( b =>
b . classList . remove ( "active" )
);
btn . classList . add ( "active" );
// Update HTML lang attribute for accessibility
document . documentElement . lang = currentLang ;
// Apply translations
applyI18n ();
// Rebuild country select with translated names
rebuildCountrySelect ( currentLang );
});
});
The document.documentElement.lang attribute improves accessibility by informing screen readers of the page language.
Country Name Translation
Country Object Structure
{
name : "Argentina" , // English
nameEs : "Argentina" , // Spanish
namePt : "Argentina" , // Portuguese
iso3 : "ARG" ,
flag : "🇦🇷"
}
Translation Function
function getCountryName ( c , lang ) {
if ( lang === "es" ) return c . nameEs || c . name ;
if ( lang === "pt" ) return c . namePt || c . name ;
return c . name ;
}
Rebuilding Country Select
function rebuildCountrySelect ( lang ) {
const input = document . getElementById ( "country" );
const datalist = document . getElementById ( "country-list" );
if ( ! input || ! datalist ) return ;
// Sort countries by localized name
const sorted = [ ... COUNTRIES ]. sort (( a , b ) =>
getCountryName ( a , lang ). localeCompare ( getCountryName ( b , lang ), lang )
);
// Rebuild datalist with translated names
datalist . innerHTML = sorted
. map ( c =>
`<option value=" ${ c . flag } ${ getCountryName ( c , lang ) } " data-iso3=" ${ c . iso3 } "></option>`
)
. join ( "" );
// Update placeholder
input . placeholder = COUNTRY_PLACEHOLDER [ lang ] || COUNTRY_PLACEHOLDER . en ;
}
Example Output (Spanish):
< option value = "🇦🇷 Argentina" data-iso3 = "ARG" ></ option >
< option value = "🇧🇷 Brasil" data-iso3 = "BRA" ></ option >
< option value = "🇨🇱 Chile" data-iso3 = "CHL" ></ option >
Translation Categories
The i18n object contains 100+ translation keys organized by category:
Key ES EN PT titleVuelta al Mundo en unos 200 Mates Around the World in about 200 Mates Volta ao Mundo em cerca de 200 Mates statMatesMates Mates Mates statCountriesPaíses Countries Países
Key ES EN PT formTitleCebate uno Share yours Compartilhe o seu placeholderNameNombre* Name* Nome* placeholderCountryPaís* Country* País* placeholderYerbaMarca de yerba* Yerba brand* Marca de erva*
Preparation Types
Key ES EN PT bitterAmargo Bitter Amargo sweetDulce Sweet Doce terereTereré Tereré Tereré brewedMateTeaMate Cocido Brewed Mate Tea Mate Cozido
Alerts
Key ES EN PT alertRequiredPor favor completá los campos… Please fill in all required… Por favor, preencha todos… alertSuccess¡Mate enviado! Aparecerá en el mapa… Mate submitted! It will appear… Mate enviado! Aparecerá no mapa… alertErrorError al enviar: Error submitting: Erro ao enviar:
Adding New Languages
Add Translation Object
Add a new language object to data/i18n.js: const i18n = {
es: { ... },
en: { ... },
pt: { ... },
fr: { // French
title: "Tour du monde en environ 200 Matés" ,
statMates: "Matés" ,
statCountries: "Pays" ,
// ... translate all keys
}
};
Add Language Button
Update the language switcher in index.html: < button class = "lang-btn" data-lang = "fr" > FR </ button >
Add Country Translations
Add nameFr property to all countries in data/countries.js: {
name : "Argentina" ,
nameEs : "Argentina" ,
namePt : "Argentina" ,
nameFr : "Argentine" , // Add this
iso3 : "ARG" ,
flag : "🇦🇷"
}
Update getCountryName()
Add French support to the function: function getCountryName ( c , lang ) {
if ( lang === "es" ) return c . nameEs || c . name ;
if ( lang === "pt" ) return c . namePt || c . name ;
if ( lang === "fr" ) return c . nameFr || c . name ; // Add this
return c . name ;
}
Test Translations
Load the site and switch to the new language to verify all translations display correctly.
Best Practices
Keys should indicate the UI location and purpose:
✅ formTitle, submitBtn, alertError
❌ text1, button2, error
All languages must have the same set of keys. Missing keys fall back to Spanish.
Avoid HTML in Translations
Store plain text in translations. Use esc() function when rendering user-generated content.
Some words have different translations based on context. Create separate keys if needed:
statMates (noun) vs matesVerb (verb)
Verify translations fit in the UI without overflow, especially for longer languages like German.
Accessibility Considerations
HTML lang Attribute
document . documentElement . lang = currentLang ;
The <html lang="es"> attribute informs:
Screen readers (correct pronunciation)
Translation tools (accurate translations)
Search engines (language targeting)
ARIA Labels
For icon-only buttons, add ARIA labels:
< button aria-label = "Submit mate" data-i18n-aria-label = "submitBtnAria" >
< i class = "icon-send" ></ i >
</ button >
Translation Contributions
From the project’s support text:
“La forma más valiosa en que nos podés ayudar es mejorando las traducciones. Si tenés sugerencias, ¡envíala a 200mates !”
Translation improvements are the most valuable contribution to the project. Contact the team to suggest better translations.
Next Steps
Module Documentation Explore the utils.js module in detail
Contributing Learn how to contribute translations