Skip to main content
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

  1. Translation Object (data/i18n.js) - All translations
  2. Translation Function (modules/utils.js) - Lookup function
  3. DOM Attributes (index.html) - Marked elements
  4. Language Switcher (app.js) - User controls

Data Structure

i18n.js:5
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:
utils.js:5
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:
  1. Try current language
  2. Fallback to Spanish (default)
  3. Return the key itself if no translation exists

DOM Integration

data-i18n Attribute

Marks elements for automatic translation:
index.html:32
<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:
index.html:100
<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:
utils.js:9
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

index.html:50
<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

app.js:5
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

countries.js:6
{ 
  name: "Argentina",        // English
  nameEs: "Argentina",       // Spanish
  namePt: "Argentina",       // Portuguese
  iso3: "ARG", 
  flag: "🇦🇷" 
}

Translation Function

countries.js:210
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

countries.js:216
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:

Header & Stats

KeyESENPT
titleVuelta al Mundo en unos 200 MatesAround the World in about 200 MatesVolta ao Mundo em cerca de 200 Mates
statMatesMatesMatesMates
statCountriesPaísesCountriesPaíses

Form Fields

KeyESENPT
formTitleCebate unoShare yoursCompartilhe o seu
placeholderNameNombre*Name*Nome*
placeholderCountryPaís*Country*País*
placeholderYerbaMarca de yerba*Yerba brand*Marca de erva*

Preparation Types

KeyESENPT
bitterAmargoBitterAmargo
sweetDulceSweetDoce
terereTereréTereréTereré
brewedMateTeaMate CocidoBrewed Mate TeaMate Cozido

Alerts

KeyESENPT
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

1

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
  }
};
2

Add Language Button

Update the language switcher in index.html:
<button class="lang-btn" data-lang="fr">FR</button>
3

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: "🇦🇷" 
}
4

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;
}
5

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.
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

app.js:11
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

Build docs developers (and LLMs) love