Skip to content
Merged

I18n #83

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ site/data/snippets.json
# Generated index page (built from templates/index.html by html-generators/)
site/index.html

# Generated locale directories (built by html-generators/ for non-English locales)
site/pt-BR/

# Platform-specific CDS/AOT intermediate files (generated by build-cds.sh)
html-generators/generate.aot
html-generators/generate.jar
Expand Down
344 changes: 302 additions & 42 deletions html-generators/generate.java

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions html-generators/locales.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# format: locale=Display name (first entry is the default/primary locale)
en=English
pt-BR=Português (Brasil)
122 changes: 114 additions & 8 deletions site/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,59 @@
(() => {
'use strict';

/* ---------- Locale Detection ---------- */
const detectLocale = () => {
const path = location.pathname;
const match = path.match(/^\/([a-z]{2}(?:-[A-Z]{2})?)\//);
return match ? match[1] : 'en';
};
const locale = detectLocale();
const localePrefix = locale === 'en' ? '' : '/' + locale;

/* ---------- Browser Locale Auto-Redirect ---------- */
const autoRedirectLocale = () => {
if (locale !== 'en') return; // already on a non-English locale
const available = (window.i18n && window.i18n.availableLocales) || [];
if (available.length <= 1) return;

// Respect explicit user choice (set when using locale picker)
const preferred = localStorage.getItem('preferred-locale');
if (preferred === 'en') return; // user explicitly chose English
if (preferred && available.includes(preferred)) {
window.location.replace('/' + preferred + location.pathname + location.search + location.hash);
return;
}

// Match browser language to available locales
const langs = navigator.languages || [navigator.language];
for (const lang of langs) {
// Exact match (e.g. pt-BR)
if (lang !== 'en' && available.includes(lang)) {
window.location.replace('/' + lang + location.pathname + location.search + location.hash);
return;
}
// Prefix match (e.g. pt matches pt-BR)
const prefix = lang.split('-')[0];
if (prefix !== 'en') {
const match = available.find(l => l.startsWith(prefix + '-') || l === prefix);
if (match) {
window.location.replace('/' + match + location.pathname + location.search + location.hash);
return;
}
}
}
};
autoRedirectLocale();

/* ---------- Snippets Data ---------- */
let snippets = [];

const loadSnippets = async () => {
try {
const res = await fetch('/data/snippets.json');
const indexPath = locale === 'en'
? '/data/snippets.json'
: '/' + locale + '/data/snippets.json';
const res = await fetch(indexPath);
snippets = await res.json();
} catch (e) {
console.warn('Could not load snippets.json:', e);
Expand Down Expand Up @@ -85,7 +132,7 @@
// Click handlers on results
resultsContainer.querySelectorAll('.search-result').forEach(el => {
el.addEventListener('click', () => {
window.location.href = '/' + el.dataset.category + '/' + el.dataset.slug + '.html';
window.location.href = localePrefix + '/' + el.dataset.category + '/' + el.dataset.slug + '.html';
});
});
};
Expand Down Expand Up @@ -144,7 +191,7 @@
} else if (e.key === 'Enter') {
e.preventDefault();
if (selectedIndex >= 0 && visibleResults[selectedIndex]) {
window.location.href = '/' + visibleResults[selectedIndex].category + '/' + visibleResults[selectedIndex].slug + '.html';
window.location.href = localePrefix + '/' + visibleResults[selectedIndex].category + '/' + visibleResults[selectedIndex].slug + '.html';
}
}
});
Expand Down Expand Up @@ -202,6 +249,9 @@
: null;
if (target) {
target.click();
// Scroll the filter section into view
const section = document.getElementById('all-comparisons');
if (section) section.scrollIntoView({ behavior: 'smooth' });
} else {
const allButton = document.querySelector('.filter-pill[data-filter="all"]');
if (allButton) allButton.click();
Expand All @@ -226,7 +276,7 @@

// Update hover hints for touch devices
document.querySelectorAll('.hover-hint').forEach(hint => {
hint.textContent = '👆 tap or swipe →';
hint.textContent = (window.i18n && window.i18n.touchHint) || '👆 tap or swipe →';
});

document.querySelectorAll('.tip-card').forEach(card => {
Expand Down Expand Up @@ -318,7 +368,7 @@
navigator.clipboard.writeText(text).then(() => {
btn.classList.add('copied');
const original = btn.textContent;
btn.textContent = 'Copied!';
btn.textContent = (window.i18n && window.i18n.copied) || 'Copied!';
setTimeout(() => {
btn.classList.remove('copied');
btn.textContent = original;
Expand All @@ -335,7 +385,7 @@
document.body.removeChild(textarea);
btn.classList.add('copied');
const original = btn.textContent;
btn.textContent = 'Copied!';
btn.textContent = (window.i18n && window.i18n.copied) || 'Copied!';
setTimeout(() => {
btn.classList.remove('copied');
btn.textContent = original;
Expand Down Expand Up @@ -550,7 +600,7 @@
if (isExpanded) {
tipsGrid.classList.add('expanded');
toggleBtn.querySelector('.view-toggle-icon').textContent = '⊟';
toggleBtn.querySelector('.view-toggle-text').textContent = 'Collapse All';
toggleBtn.querySelector('.view-toggle-text').textContent = (window.i18n && window.i18n.collapseAll) || 'Collapse All';

// Remove toggled class from all cards when expanding
document.querySelectorAll('.tip-card').forEach(card => {
Expand All @@ -559,7 +609,7 @@
} else {
tipsGrid.classList.remove('expanded');
toggleBtn.querySelector('.view-toggle-icon').textContent = '⊞';
toggleBtn.querySelector('.view-toggle-text').textContent = 'Expand All';
toggleBtn.querySelector('.view-toggle-text').textContent = (window.i18n && window.i18n.expandAll) || 'Expand All';
}
});

Expand Down Expand Up @@ -593,6 +643,61 @@
});
};

/* ==========================================================
8. Locale Picker
========================================================== */
const initLocalePicker = () => {
const picker = document.getElementById('localePicker');
if (!picker) return;

const toggleBtn = picker.querySelector('.locale-toggle');
const list = picker.querySelector('ul');

const open = () => {
list.style.display = 'block';
toggleBtn.setAttribute('aria-expanded', 'true');
};

const close = () => {
list.style.display = 'none';
toggleBtn.setAttribute('aria-expanded', 'false');
};

toggleBtn.addEventListener('click', (e) => {
e.stopPropagation();
list.style.display === 'block' ? close() : open();
});

document.addEventListener('click', close);
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') close();
});

list.querySelectorAll('li').forEach(li => {
li.addEventListener('click', (e) => {
e.stopPropagation();
const targetLocale = li.dataset.locale;
if (targetLocale === locale) { close(); return; }

// Rewrite current path for the target locale
let path = location.pathname;

// Strip current locale prefix if present
if (locale !== 'en') {
path = path.replace(new RegExp('^/' + locale.replace('-', '\\-')), '');
}

// Add target locale prefix
if (targetLocale !== 'en') {
path = '/' + targetLocale + path;
}

localStorage.setItem('preferred-locale', targetLocale);
window.location.href = path;
});
});
};

/* ==========================================================
Utilities
========================================================== */
Expand All @@ -616,5 +721,6 @@
initSyntaxHighlighting();
initNewsletter();
initThemeToggle();
initLocalePicker();
});
})();
81 changes: 81 additions & 0 deletions site/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -1572,3 +1572,84 @@ footer a:hover {
}
.share-li { font-family: Georgia, serif; font-weight: 800; font-size: 0.85rem; }
.share-reddit { font-size: 1.1rem; }

/* ============================
Locale Picker
============================ */
.locale-picker {
position: relative;
display: inline-flex;
}
.locale-toggle {
display: flex;
align-items: center;
justify-content: center;
padding: 8px;
border-radius: var(--radius-sm);
border: 1px solid var(--border);
background: var(--surface);
color: var(--text-muted);
font-size: 1rem;
line-height: 1;
transition: all 0.2s;
cursor: pointer;
}
.locale-toggle:hover {
background: var(--accent);
color: #fff;
border-color: var(--accent);
}
.locale-picker ul {
display: none;
position: absolute;
top: 100%;
right: 0;
margin: 4px 0 0;
padding: 4px 0;
list-style: none;
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius-sm);
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
z-index: 100;
min-width: 160px;
}
.locale-picker li {
padding: 8px 16px;
cursor: pointer;
font-size: 0.9rem;
color: var(--text);
white-space: nowrap;
transition: background 0.15s;
}
.locale-picker li:hover {
background: var(--accent);
color: #fff;
}
.locale-picker li.active {
font-weight: 600;
color: var(--accent);
}
.locale-picker li.active:hover {
color: #fff;
}

/* ============================
Untranslated Notice Banner
============================ */
.untranslated-notice {
background: var(--surface);
border: 1px solid var(--border);
border-left: 4px solid var(--accent);
padding: 12px 16px;
margin: 0 auto 24px;
max-width: 800px;
border-radius: var(--radius-sm);
font-size: 0.9rem;
color: var(--text-muted);
}
.untranslated-notice a {
color: var(--accent);
text-decoration: underline;
margin-left: 8px;
}
8 changes: 4 additions & 4 deletions templates/index-card.html
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<a href="/{{category}}/{{slug}}.html" class="tip-card filter-hidden" data-category="{{category}}">
<a href="{{cardHref}}" class="tip-card filter-hidden" data-category="{{category}}">
<div class="tip-card-body">
<div class="tip-card-header">
<div class="tip-badges"><span class="badge {{category}}">{{catDisplay}}</span></div>
Expand All @@ -7,14 +7,14 @@ <h3>{{title}}</h3>
</div>
<div class="card-code">
<div class="card-code-layer old-layer">
<span class="mini-label">Old</span>
<span class="mini-label">{{cards.old}}</span>
<span class="code-text">{{oldCode}}</span>
</div>
<div class="card-code-layer modern-layer">
<span class="mini-label">Modern</span>
<span class="mini-label">{{cards.modern}}</span>
<span class="code-text">{{modernCode}}</span>
</div>
<span class="hover-hint">hover to see modern →</span>
<span class="hover-hint">{{cards.hoverHint}}</span>
</div>
<div class="tip-card-footer">
<span class="browser-support"><span class="dot"></span> JDK {{jdkVersion}}+</span>
Expand Down
Loading