The construct from what I have read and seen is that Jekyll's parser needs to read a JSON file and from my main.js it can toggle between ENG and JPN on every site, and be kept on the chosen language until switched. Even after selecting the language, going back to the site index will remain on the same selected language because of localStorage.
This was something new to me, at least the name. It's stored in structured formats like JSON and YAML. Interestingly, it also handles locale-specific information like date and time formats.
These three sections will spread across the entire site and be added as Jekyll includes.
For this to work, EVERYTHING MUST CHANGE. That's just the way it is.
To make this page translatable using the data-i18n system, everything must be translated with data-i18n attributes.
const i18n = {
currentLang: localStorage.getItem('siteLang') || 'en',
translations: {},
...
};
Each file contains translation keys and is placed in the root of the project
{
"siteTitle": "My Portfolio",
"welcomeMessage": "Welcome to my site!",
"aboutMe": "About Me",
"contact": "Contact",
"projects": "Projects"
}
{
"siteTitle": "私のポートフォリオ",
"welcomeMessage": "私のサイトへようこそ!",
"aboutMe": "自己紹介",
"contact": "お問い合わせ",
"projects": "プロジェクト"
}
It's as if a whole new page is created instantly when switching languages!
// =====================================================
// i18n TRANSLATION SYSTEM (FIXED)
// =====================================================
const translations = {};
let currentLang = 'en';
// Load language file
function loadLanguage(lang) {
// Only load JSON for non-English languages
if (lang === 'en') {
currentLang = 'en';
applyTranslations('en');
localStorage.setItem('lang', 'en');
updateLanguageButtons('en');
return Promise.resolve();
}
return fetch(`/${lang}.json`)
.then(res => {
if (!res.ok) throw new Error(`Language file not found: ${lang}.json`);
return res.json();
})
.then(data => {
translations[lang] = data;
currentLang = lang;
applyTranslations(lang);
localStorage.setItem('lang', lang);
updateLanguageButtons(lang);
})
.catch(err => {
console.error('i18n error:', err);
if (lang !== 'en') loadLanguage('en');
});
}
// Apply translations to all elements with data-i18n
function applyTranslations(lang) {
document.querySelectorAll('[data-i18n]').forEach(el => {
const key = el.getAttribute('data-i18n');
if (lang === 'en') {
// Fallback to original HTML content
el.textContent = el.dataset.originalText || el.textContent;
} else if (translations[lang] && translations[lang][key]) {
// Store original text first time
if (!el.dataset.originalText) el.dataset.originalText = el.textContent;
el.textContent = translations[lang][key];
}
});
// Update <title>
const titleEl = document.querySelector('title');
if (titleEl) {
if (!titleEl.dataset.titleEn) titleEl.dataset.titleEn = titleEl.textContent;
if (!titleEl.dataset.titleJa) titleEl.dataset.titleJa = titleEl.dataset.titleEn;
titleEl.textContent = (lang === 'ja')
? titleEl.dataset.titleJa
: titleEl.dataset.titleEn;
}
// Apply translations to blog cards
document.querySelectorAll('.blog-card').forEach(card => {
const titleEl = card.querySelector('.blog-card-title');
const excerptEl = card.querySelector('.blog-card-excerpt');
if (titleEl) {
titleEl.textContent = lang === 'ja'
? card.getAttribute('data-title-ja')
: card.getAttribute('data-title-en');
}
if (excerptEl) {
excerptEl.textContent = lang === 'ja'
? card.getAttribute('data-excerpt-ja')
: card.getAttribute('data-excerpt-en');
}
});
}
// Update active button state
function updateLanguageButtons(lang) {
document.querySelectorAll('.lang-btn').forEach(btn => {
btn.classList.toggle('active-lang', btn.dataset.lang === lang);
});
}
// Initialize language buttons
function initLanguageButtons() {
document.querySelectorAll('.lang-btn').forEach(btn => {
btn.addEventListener('click', e => {
e.preventDefault();
loadLanguage(btn.dataset.lang);
});
});
}
// DOM ready
document.addEventListener('DOMContentLoaded', () => {
initLanguageButtons();
const savedLang = localStorage.getItem('lang') || 'en';
loadLanguage(savedLang);
});