Add Russian translations and pronunciation button
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
- Russian fields on Mineral model (name_ru, description_ru, history_ru, etc.) - scrape_minerals_ru management command fetches from Russian Wikipedia via langlinks - EN/RU toggle in header, saved to localStorage - Speaker button next to mineral name uses Web Speech API - Section headers and labels translated - Russian Wikipedia link in footer when in RU mode Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -69,13 +69,15 @@
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.search-toggle {
|
||||
.header-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
min-width: 34px;
|
||||
padding: 0 0.5rem;
|
||||
font-family: 'Georgia', serif;
|
||||
font-size: 0.8rem;
|
||||
color: var(--stone-text);
|
||||
background: var(--stone-muted);
|
||||
border: 1px solid var(--border);
|
||||
@@ -85,15 +87,55 @@
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.search-toggle:hover {
|
||||
.header-btn:hover {
|
||||
background: color-mix(in srgb, var(--stone-color) 20%, #f5f5f0);
|
||||
}
|
||||
|
||||
.search-toggle svg {
|
||||
.header-btn svg {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
}
|
||||
|
||||
.header-btn.active {
|
||||
background: var(--stone-color);
|
||||
color: #fff;
|
||||
border-color: var(--stone-color);
|
||||
}
|
||||
|
||||
.random-btn {
|
||||
gap: 0.35rem;
|
||||
padding: 0 0.65rem;
|
||||
}
|
||||
|
||||
.lang-toggle {
|
||||
display: flex;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 3px;
|
||||
overflow: hidden;
|
||||
height: 34px;
|
||||
}
|
||||
|
||||
.lang-toggle button {
|
||||
padding: 0 0.5rem;
|
||||
font-family: 'Georgia', serif;
|
||||
font-size: 0.75rem;
|
||||
font-weight: bold;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
background: var(--stone-muted);
|
||||
color: var(--stone-text);
|
||||
transition: background 0.2s, color 0.2s;
|
||||
}
|
||||
|
||||
.lang-toggle button:first-child {
|
||||
border-right: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.lang-toggle button.active {
|
||||
background: var(--stone-color);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.search-bar {
|
||||
display: none;
|
||||
margin: -0.5rem 0 1.5rem;
|
||||
@@ -139,43 +181,51 @@
|
||||
background: color-mix(in srgb, var(--stone-color) 20%, #f5f5f0);
|
||||
}
|
||||
|
||||
.random-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.35rem;
|
||||
padding: 0.4rem 0.65rem;
|
||||
font-family: 'Georgia', serif;
|
||||
font-size: 0.8rem;
|
||||
color: var(--stone-text);
|
||||
background: var(--stone-muted);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 3px;
|
||||
text-decoration: none;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.random-btn:hover {
|
||||
background: color-mix(in srgb, var(--stone-color) 20%, #f5f5f0);
|
||||
}
|
||||
|
||||
.random-btn svg {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
}
|
||||
|
||||
.page-header .date {
|
||||
font-size: 0.85rem;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.mineral-name-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.5rem;
|
||||
margin: 0.5rem 0 0.25rem;
|
||||
}
|
||||
|
||||
.mineral-name {
|
||||
font-size: 2.4rem;
|
||||
font-weight: normal;
|
||||
color: var(--stone-text);
|
||||
margin: 0.5rem 0 0.25rem;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.speak-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
background: none;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
color: var(--text-secondary);
|
||||
transition: color 0.2s, border-color 0.2s;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.speak-btn:hover {
|
||||
color: var(--stone-text);
|
||||
border-color: var(--border);
|
||||
}
|
||||
|
||||
.speak-btn svg {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
|
||||
.formula {
|
||||
font-size: 1.15rem;
|
||||
font-family: 'Georgia', serif;
|
||||
@@ -191,6 +241,16 @@
|
||||
top: 0.3em;
|
||||
}
|
||||
|
||||
/* Language content */
|
||||
.lang-ru { display: none; }
|
||||
|
||||
.mineral-name-ru {
|
||||
font-size: 1.1rem;
|
||||
color: var(--text-secondary);
|
||||
margin-top: -0.15rem;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
/* Photo gallery */
|
||||
.gallery {
|
||||
margin: 1.5rem 0;
|
||||
@@ -397,12 +457,18 @@
|
||||
|
||||
<header class="page-header">
|
||||
<div class="header-actions">
|
||||
<a href="#" class="search-toggle" title="Search minerals" onclick="document.getElementById('searchBar').classList.toggle('open');document.getElementById('searchInput').focus();return false;">
|
||||
<a href="#" class="header-btn" title="Search minerals" onclick="document.getElementById('searchBar').classList.toggle('open');document.getElementById('searchInput').focus();return false;">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line>
|
||||
</svg>
|
||||
</a>
|
||||
<a href="{% url 'dailystone:random_stone' %}" class="random-btn" title="Random mineral">
|
||||
{% if mineral.name_ru %}
|
||||
<div class="lang-toggle">
|
||||
<button onclick="setLang('en')" data-lang="en" class="active">EN</button>
|
||||
<button onclick="setLang('ru')" data-lang="ru">RU</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
<a href="{% url 'dailystone:random_stone' %}" class="header-btn random-btn" title="Random mineral">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<polyline points="16 3 21 3 21 8"></polyline><line x1="4" y1="20" x2="21" y2="3"></line>
|
||||
<polyline points="21 16 21 21 16 21"></polyline><line x1="15" y1="15" x2="21" y2="21"></line>
|
||||
@@ -412,7 +478,25 @@
|
||||
</a>
|
||||
</div>
|
||||
<div class="label">{% if is_random %}Random Stone{% else %}Daily Stone{% endif %}</div>
|
||||
<h1 class="mineral-name">{{ mineral.name }}</h1>
|
||||
<div class="mineral-name-row">
|
||||
<h1 class="mineral-name">
|
||||
<span class="lang-en">{{ mineral.name }}</span>
|
||||
{% if mineral.name_ru %}<span class="lang-ru">{{ mineral.name_ru }}</span>{% endif %}
|
||||
</h1>
|
||||
<button class="speak-btn" onclick="speak('{{ mineral.name }}')" title="Pronounce name">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5"></polygon>
|
||||
<path d="M15.54 8.46a5 5 0 0 1 0 7.07"></path>
|
||||
<path d="M19.07 4.93a10 10 0 0 1 0 14.14"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
{% if mineral.name_ru %}
|
||||
<div class="mineral-name-ru">
|
||||
<span class="lang-en">{{ mineral.name_ru }}</span>
|
||||
<span class="lang-ru">{{ mineral.name }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if mineral.formula %}<div class="formula">{{ mineral.formula|chem_formula }}</div>{% endif %}
|
||||
<div class="date">{{ today|date:"F j, Y" }}</div>
|
||||
</header>
|
||||
@@ -449,52 +533,57 @@
|
||||
<div class="color-row">
|
||||
<div class="color-swatch"></div>
|
||||
<div class="color-info">
|
||||
<span class="lang-en">
|
||||
{% if mineral.color_description %}
|
||||
{{ mineral.color_description }}
|
||||
{% else %}
|
||||
Typical color
|
||||
{% endif %}
|
||||
</span>
|
||||
{% if mineral.color_description_ru %}
|
||||
<span class="lang-ru">{{ mineral.color_description_ru }}</span>
|
||||
{% endif %}
|
||||
<br><span class="hex">{{ mineral.color_hex }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Properties -->
|
||||
<div class="properties">
|
||||
<h2>Properties</h2>
|
||||
<h2><span class="lang-en">Properties</span><span class="lang-ru">Свойства</span></h2>
|
||||
<div class="prop-grid">
|
||||
{% if mineral.category %}
|
||||
<div class="prop-item">
|
||||
<span class="prop-label">Category</span>
|
||||
<span class="prop-label"><span class="lang-en">Category</span><span class="lang-ru">Категория</span></span>
|
||||
<span class="prop-value">{{ mineral.category }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if mineral.crystal_system %}
|
||||
<div class="prop-item">
|
||||
<span class="prop-label">Crystal System</span>
|
||||
<span class="prop-label"><span class="lang-en">Crystal System</span><span class="lang-ru">Сингония</span></span>
|
||||
<span class="prop-value">{{ mineral.crystal_system }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if mineral.mohs_hardness %}
|
||||
<div class="prop-item">
|
||||
<span class="prop-label">Hardness (Mohs)</span>
|
||||
<span class="prop-label"><span class="lang-en">Hardness (Mohs)</span><span class="lang-ru">Твёрдость (Моос)</span></span>
|
||||
<span class="prop-value">{{ mineral.mohs_hardness }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if mineral.luster %}
|
||||
<div class="prop-item">
|
||||
<span class="prop-label">Luster</span>
|
||||
<span class="prop-label"><span class="lang-en">Luster</span><span class="lang-ru">Блеск</span></span>
|
||||
<span class="prop-value">{{ mineral.luster }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if mineral.streak %}
|
||||
<div class="prop-item">
|
||||
<span class="prop-label">Streak</span>
|
||||
<span class="prop-label"><span class="lang-en">Streak</span><span class="lang-ru">Черта</span></span>
|
||||
<span class="prop-value">{{ mineral.streak }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if mineral.specific_gravity %}
|
||||
<div class="prop-item">
|
||||
<span class="prop-label">Specific Gravity</span>
|
||||
<span class="prop-label"><span class="lang-en">Specific Gravity</span><span class="lang-ru">Плотность</span></span>
|
||||
<span class="prop-value">{{ mineral.specific_gravity }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
@@ -504,32 +593,66 @@
|
||||
<!-- Description -->
|
||||
{% if mineral.description %}
|
||||
<div class="section">
|
||||
<h2>About</h2>
|
||||
<h2><span class="lang-en">About</span><span class="lang-ru">Описание</span></h2>
|
||||
<div class="lang-en">
|
||||
{% for para in mineral.description.splitlines %}
|
||||
{% if para %}<p>{{ para }}</p>{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% if mineral.description_ru %}
|
||||
<div class="lang-ru">
|
||||
{% for para in mineral.description_ru.splitlines %}
|
||||
{% if para %}<p>{{ para }}</p>{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- History -->
|
||||
{% if mineral.history %}
|
||||
<div class="section">
|
||||
<h2>History & Etymology</h2>
|
||||
<h2><span class="lang-en">History & Etymology</span><span class="lang-ru">История и этимология</span></h2>
|
||||
<div class="lang-en">
|
||||
{% for para in mineral.history.splitlines %}
|
||||
{% if para %}<p>{{ para }}</p>{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% if mineral.history_ru %}
|
||||
<div class="lang-ru">
|
||||
{% for para in mineral.history_ru.splitlines %}
|
||||
{% if para %}<p>{{ para }}</p>{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Footer -->
|
||||
<footer class="page-footer">
|
||||
<span class="lang-en">
|
||||
{% if mineral.wikipedia_url %}
|
||||
<a href="{{ mineral.wikipedia_url }}" target="_blank" rel="noopener">
|
||||
Read more on Wikipedia →
|
||||
</a>
|
||||
{% endif %}
|
||||
</span>
|
||||
<span class="lang-ru">
|
||||
{% if mineral.wikipedia_url_ru %}
|
||||
<a href="{{ mineral.wikipedia_url_ru }}" target="_blank" rel="noopener">
|
||||
Читать на Википедии →
|
||||
</a>
|
||||
{% elif mineral.wikipedia_url %}
|
||||
<a href="{{ mineral.wikipedia_url }}" target="_blank" rel="noopener">
|
||||
Read more on Wikipedia →
|
||||
</a>
|
||||
{% endif %}
|
||||
</span>
|
||||
{% if total_minerals %}
|
||||
<span class="mineral-count">{{ total_minerals }} minerals in collection</span>
|
||||
<span class="mineral-count">
|
||||
<span class="lang-en">{{ total_minerals }} minerals in collection</span>
|
||||
<span class="lang-ru">{{ total_minerals }} минералов в коллекции</span>
|
||||
</span>
|
||||
{% endif %}
|
||||
<br>
|
||||
<a href="/" class="home-link">k-boris.tech</a>
|
||||
@@ -543,6 +666,34 @@
|
||||
);
|
||||
thumb.classList.add('active');
|
||||
}
|
||||
|
||||
function speak(text) {
|
||||
if (!('speechSynthesis' in window)) return;
|
||||
speechSynthesis.cancel();
|
||||
var u = new SpeechSynthesisUtterance(text);
|
||||
u.lang = 'en-US';
|
||||
u.rate = 0.85;
|
||||
speechSynthesis.speak(u);
|
||||
}
|
||||
|
||||
function setLang(lang) {
|
||||
var en = document.querySelectorAll('.lang-en');
|
||||
var ru = document.querySelectorAll('.lang-ru');
|
||||
for (var i = 0; i < en.length; i++) en[i].style.display = lang === 'en' ? '' : 'none';
|
||||
for (var i = 0; i < ru.length; i++) ru[i].style.display = lang === 'ru' ? '' : 'none';
|
||||
var btns = document.querySelectorAll('.lang-toggle button');
|
||||
for (var i = 0; i < btns.length; i++) {
|
||||
btns[i].classList.toggle('active', btns[i].getAttribute('data-lang') === lang);
|
||||
}
|
||||
document.documentElement.lang = lang;
|
||||
try { localStorage.setItem('dailystone-lang', lang); } catch(e) {}
|
||||
}
|
||||
|
||||
// Restore saved language preference
|
||||
try {
|
||||
var saved = localStorage.getItem('dailystone-lang');
|
||||
if (saved === 'ru') setLang('ru');
|
||||
} catch(e) {}
|
||||
</script>
|
||||
|
||||
{% else %}
|
||||
|
||||
Reference in New Issue
Block a user