Skip to main content

Overview

Dev Showcase implements a client-side localization system using JSON files for English and Spanish translations. The system supports dynamic language switching without page reloads and persists user language preferences using cookies.

Architecture Components

Language Files

JSON files containing all translatable content

Cookie Persistence

Stores user’s language preference

JavaScript Loader

Dynamically loads and applies translations

Language Files Structure

Translations are stored in JSON files located in wwwroot/languages/:
wwwroot/
└── languages/
    ├── en.json    # English translations
    └── es.json    # Spanish translations

JSON Structure

Each language file contains hierarchically organized content:
wwwroot/languages/en.json:1
{
  "typewriter": {
    "prefix": "I'm ",
    "phrases": [
      "Ricardo Alejandro Pérez Santillán",
      "a Data enthusiast",
      "an automation addict"
    ]
  },
  "header": {
    "hello": "#HELLO WORLD",
    "name": "I'm Ricardo Alejandro Pérez Santillán"
  },
  "navigation": {
    "introduction": "Introduction",
    "skills": "Skills",
    "projects": "Projects & Experience",
    "education": "Education",
    "downloadCV": "Download CV"
  }
}

Profile-Specific Content

The system supports different content based on the selected profile:
wwwroot/languages/en.json:31
"introduction": {
  "aboutTitle": "About Me",
  "aboutContent_dataScience": "I am a software developer with a recent focus on Data Science...",
  "aboutContent_webDev": "Web developer with a background in Information Technology...",
  "aboutContent_dataAnalyst": "Data Analyst with practical experience in data analysis..."
}
Content keys use the pattern {key}_{profile} to load profile-specific translations dynamically.

Language Switching Mechanism

Server-Side: SetLanguage Endpoint

The HomeController provides an endpoint to persist language preferences:
Controllers/HomeController.cs:29
[HttpPost]
public IActionResult SetLanguage(string lang)
{
    Response.Cookies.Append("lang", lang ?? "es", new CookieOptions
    {
        Expires = DateTimeOffset.UtcNow.AddYears(1),
        IsEssential = true
    });
    return Ok();
}
Expires
DateTimeOffset
Cookie expires after 1 year
IsEssential
bool
Set to true - cookie is required for site functionality
lang
string
default:"es"
Defaults to Spanish if no language specified
The cookie is marked as essential because language selection is fundamental to the user experience. This means it will be set even if users decline non-essential cookies.

Client-Side: JavaScript Implementation

The language switching is handled by JavaScript in translation.js:
wwwroot/js/translation.js
// Simplified example of the translation system
let currentLang = getCookie('lang') || 'es';
let translations = {};

async function loadLanguage(lang) {
    const response = await fetch(`/languages/${lang}.json`);
    translations = await response.json();
    applyTranslations();
}

function applyTranslations() {
    document.querySelectorAll('[data-i18n]').forEach(element => {
        const key = element.getAttribute('data-i18n');
        const value = getNestedValue(translations, key);
        if (value) element.textContent = value;
    });
}

function switchLanguage(newLang) {
    fetch('/Home/SetLanguage', {
        method: 'POST',
        headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
        body: `lang=${newLang}`
    });
    
    currentLang = newLang;
    loadLanguage(newLang);
}

How Language Loading Works

1

Page loads

JavaScript checks for the lang cookie
2

Determine language

Use cookie value if present, otherwise default to Spanish (es)
3

Fetch JSON file

Make async request to /languages/{lang}.json
4

Parse translations

Convert JSON response to JavaScript object
5

Apply to DOM

Find all elements with data-i18n attributes and update their content
6

Update profile content

Load profile-specific content using the current profile from ViewData

Visual Flow

Using Translations in HTML

To make content translatable, use the data-i18n attribute:
<!-- Simple translation -->
<h2 data-i18n="navigation.skills"></h2>
<!-- Renders: "Skills" (en) or "Habilidades" (es) -->

<!-- Nested property -->
<p data-i18n="introduction.aboutTitle"></p>
<!-- Renders: "About Me" (en) or "Acerca de Mí" (es) -->

<!-- Profile-specific content -->
<div data-i18n="introduction.aboutContent_dataScience"></div>
<!-- Uses current profile to load correct content -->

Content Organization

The JSON files are organized into logical sections:

Main Sections

Animated text for the typewriter effect on the homepage
Main header content and greeting
About me section with profile-specific content
Skills section labels, categories, and progress bars
Work experience and personal projects descriptions
Academic history and certificates
Numerical data for vocational charts (RIASEC, cognitive scores)

Adding a New Language

To add support for a new language (e.g., French):
1

Create language file

Create wwwroot/languages/fr.json with all translated content:
{
  "typewriter": {
    "prefix": "Je suis ",
    "phrases": [
      "Ricardo Alejandro Pérez Santillán",
      "un passionné de données",
      "accro à l'automatisation"
    ]
  },
  "navigation": {
    "introduction": "Introduction",
    "skills": "Compétences",
    "projects": "Projets & Expérience",
    "education": "Éducation"
  }
}
2

Update route constraints

Add “fr” to the language regex in Program.cs:
app.MapGet("/{lang:regex(^(es|en|fr)$)}", (string lang, HttpContext context) =>
{
    context.Response.Redirect($"/{lang}/dataScience", permanent: false);
    return Task.CompletedTask;
});

// Also update in MapControllerRoute constraints
constraints: new
{
    lang = "^(es|en|fr)$",
    profile = @"^(dataScience|webDev|dataAnalyst|DataAnalysis)$"
}
3

Update UI language selector

Add a button/option for French in your language switcher UI:
<button onclick="switchLanguage('fr')">Français</button>
4

Test thoroughly

Ensure all content sections have French translations and display correctly
Use a tool like i18next-scanner or create a script to identify missing translation keys across language files.

Content Keys Convention

Follow these naming conventions for consistency:
PatternExampleUsage
section.keynavigation.skillsSimple property
section.key_profileintroduction.aboutContent_webDevProfile-specific
section.subsection.keyskills.panels.webDevNested properties
section.list[]typewriter.phrases[0]Array values

Dynamic Content Loading

For content that changes based on user interaction:
// Load profile-specific content
function loadProfileContent(profile) {
    const aboutKey = `introduction.aboutContent_${profile}`;
    const content = getNestedValue(translations, aboutKey);
    document.querySelector('#about-text').textContent = content;
}

// Load when profile changes
document.addEventListener('profileChanged', (event) => {
    loadProfileContent(event.detail.profile);
});

Benefits of This Approach

No Page Reload

Language changes happen instantly without refreshing the page

Easy Maintenance

All translations in JSON files - no need to rebuild or redeploy

SEO-Friendly URLs

URLs like /en/dataScience are clear and indexable

Scalable

Adding new languages is straightforward - just add a JSON file

Best Practices

Use the same key structure across all language files. Missing keys will result in blank content.
Use a JSON validator to ensure files are properly formatted before deployment.
Implement fallback logic to show default language if translation is missing:
const value = translations[lang]?.[key] || translations['es']?.[key] || key;
Track translation changes in git to see content evolution over time.

Troubleshooting

Issue: Changed JSON but content still shows old valuesSolution: Clear browser cache or do a hard refresh (Ctrl+F5)
Issue: Some elements show blank or keys instead of translated textSolution: Compare JSON files to find missing keys. Use a diff tool to identify discrepancies between language files.
Issue: Translations fail to loadSolution: Validate JSON syntax. Common issues:
  • Missing commas
  • Trailing commas (invalid in JSON)
  • Unescaped quotes in strings
  • Incorrect UTF-8 encoding

Performance Considerations

The language JSON files are loaded once per session and cached by the browser. However, consider these optimizations for larger applications:
  • Lazy loading: Only load sections as needed
  • Compression: Enable gzip compression for JSON files
  • CDN: Serve language files from a CDN for global users
  • LocalStorage: Cache translations in localStorage to reduce network requests

Next Steps

Routing

Learn how language codes are integrated into URL routing

Architecture Overview

Return to the architecture overview

Build docs developers (and LLMs) love