Add Russian translations for mineral property values
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
- Add properties_ru JSON field to store translated category, crystal_system, luster, streak, specific_gravity, color_description - Add translate_minerals management command using deep-translator (Google Translate), with a hard-coded dictionary for crystal systems - Template: show translated values in RU mode, fall back to English if missing - Add deep-translator to requirements.txt Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
120
dailystone/management/commands/translate_minerals.py
Normal file
120
dailystone/management/commands/translate_minerals.py
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
"""
|
||||||
|
Translate mineral property values (category, crystal_system, luster, streak,
|
||||||
|
specific_gravity, color_description) into Russian using Google Translate via
|
||||||
|
the deep-translator library. Results are stored in the properties_ru JSON field.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
python manage.py translate_minerals # translate all
|
||||||
|
python manage.py translate_minerals --skip-existing # skip already done
|
||||||
|
python manage.py translate_minerals --limit 50 # translate first 50
|
||||||
|
"""
|
||||||
|
|
||||||
|
import time
|
||||||
|
|
||||||
|
from django.core.management.base import BaseCommand
|
||||||
|
|
||||||
|
from dailystone.models import Mineral
|
||||||
|
|
||||||
|
# Hard-coded dictionary for standard mineralogical terms that translate
|
||||||
|
# inconsistently with machine translation.
|
||||||
|
CRYSTAL_SYSTEMS = {
|
||||||
|
'triclinic': 'Триклинная',
|
||||||
|
'monoclinic': 'Моноклинная',
|
||||||
|
'orthorhombic': 'Ромбическая',
|
||||||
|
'tetragonal': 'Тетрагональная',
|
||||||
|
'trigonal': 'Тригональная',
|
||||||
|
'hexagonal': 'Гексагональная',
|
||||||
|
'cubic': 'Кубическая',
|
||||||
|
'isometric': 'Кубическая',
|
||||||
|
'amorphous': 'Аморфная',
|
||||||
|
'rhombohedral': 'Ромбоэдрическая',
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _dict_translate(value, lookup):
|
||||||
|
"""Try a case-insensitive dictionary lookup, return None on miss."""
|
||||||
|
if not value:
|
||||||
|
return None
|
||||||
|
key = value.strip().lower()
|
||||||
|
for k, v in lookup.items():
|
||||||
|
if k in key:
|
||||||
|
return v
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
class Command(BaseCommand):
|
||||||
|
help = 'Translate mineral property values to Russian using Google Translate'
|
||||||
|
|
||||||
|
def add_arguments(self, parser):
|
||||||
|
parser.add_argument('--limit', type=int, default=0,
|
||||||
|
help='Max minerals to process (0 = all)')
|
||||||
|
parser.add_argument('--skip-existing', action='store_true',
|
||||||
|
help='Skip minerals that already have properties_ru')
|
||||||
|
|
||||||
|
def handle(self, *args, **options):
|
||||||
|
try:
|
||||||
|
from deep_translator import GoogleTranslator
|
||||||
|
except ImportError:
|
||||||
|
self.stderr.write('deep-translator not installed. Run: pip install deep-translator')
|
||||||
|
return
|
||||||
|
|
||||||
|
translator = GoogleTranslator(source='en', target='ru')
|
||||||
|
|
||||||
|
qs = Mineral.objects.all()
|
||||||
|
if options['skip_existing']:
|
||||||
|
qs = qs.exclude(properties_ru__isnull=False).exclude(properties_ru={})
|
||||||
|
if options['limit']:
|
||||||
|
qs = qs[:options['limit']]
|
||||||
|
|
||||||
|
total = qs.count()
|
||||||
|
self.stdout.write(f'Translating properties for {total} minerals...')
|
||||||
|
|
||||||
|
done = 0
|
||||||
|
errors = 0
|
||||||
|
|
||||||
|
for mineral in qs:
|
||||||
|
props = dict(mineral.properties_ru or {})
|
||||||
|
changed = False
|
||||||
|
|
||||||
|
fields = {
|
||||||
|
'category': mineral.category,
|
||||||
|
'crystal_system': mineral.crystal_system,
|
||||||
|
'luster': mineral.luster,
|
||||||
|
'streak': mineral.streak,
|
||||||
|
'specific_gravity': mineral.specific_gravity,
|
||||||
|
'color_description': mineral.color_description_ru or mineral.color_description,
|
||||||
|
}
|
||||||
|
|
||||||
|
for key, value in fields.items():
|
||||||
|
if not value or key in props:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Try dictionary first for crystal system
|
||||||
|
if key == 'crystal_system':
|
||||||
|
result = _dict_translate(value, CRYSTAL_SYSTEMS)
|
||||||
|
if result:
|
||||||
|
props[key] = result
|
||||||
|
changed = True
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Machine translate
|
||||||
|
try:
|
||||||
|
translated = translator.translate(value)
|
||||||
|
if translated and translated != value:
|
||||||
|
props[key] = translated
|
||||||
|
changed = True
|
||||||
|
time.sleep(0.3)
|
||||||
|
except Exception as e:
|
||||||
|
self.stderr.write(f' [{mineral.name}] {key}: {e}')
|
||||||
|
errors += 1
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
|
if changed:
|
||||||
|
mineral.properties_ru = props
|
||||||
|
mineral.save(update_fields=['properties_ru'])
|
||||||
|
done += 1
|
||||||
|
self.stdout.write(f' [{done}/{total}] {mineral.name}')
|
||||||
|
|
||||||
|
self.stdout.write(self.style.SUCCESS(
|
||||||
|
f'Done. Translated {done} minerals, {errors} errors.'
|
||||||
|
))
|
||||||
16
dailystone/migrations/0003_mineral_properties_ru.py
Normal file
16
dailystone/migrations/0003_mineral_properties_ru.py
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('dailystone', '0002_add_russian_fields'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='mineral',
|
||||||
|
name='properties_ru',
|
||||||
|
field=models.JSONField(blank=True, default=dict),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -19,6 +19,7 @@ class Mineral(models.Model):
|
|||||||
day_of_year = models.IntegerField(unique=True, null=True, blank=True)
|
day_of_year = models.IntegerField(unique=True, null=True, blank=True)
|
||||||
|
|
||||||
# Russian translations
|
# Russian translations
|
||||||
|
properties_ru = models.JSONField(default=dict, blank=True)
|
||||||
name_ru = models.CharField(max_length=200, blank=True)
|
name_ru = models.CharField(max_length=200, blank=True)
|
||||||
description_ru = models.TextField(blank=True)
|
description_ru = models.TextField(blank=True)
|
||||||
history_ru = models.TextField(blank=True)
|
history_ru = models.TextField(blank=True)
|
||||||
|
|||||||
@@ -533,16 +533,8 @@
|
|||||||
<div class="color-row">
|
<div class="color-row">
|
||||||
<div class="color-swatch"></div>
|
<div class="color-swatch"></div>
|
||||||
<div class="color-info">
|
<div class="color-info">
|
||||||
<span class="lang-en">
|
<span class="lang-en">{{ mineral.color_description|default:"Typical color" }}</span>
|
||||||
{% if mineral.color_description %}
|
<span class="lang-ru">{{ mineral.color_description_ru|default:mineral.properties_ru.color_description|default:mineral.color_description|default:"Типичный цвет" }}</span>
|
||||||
{{ 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>
|
<br><span class="hex">{{ mineral.color_hex }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -554,13 +546,19 @@
|
|||||||
{% if mineral.category %}
|
{% if mineral.category %}
|
||||||
<div class="prop-item">
|
<div class="prop-item">
|
||||||
<span class="prop-label"><span class="lang-en">Category</span><span class="lang-ru">Категория</span></span>
|
<span class="prop-label"><span class="lang-en">Category</span><span class="lang-ru">Категория</span></span>
|
||||||
<span class="prop-value">{{ mineral.category }}</span>
|
<span class="prop-value">
|
||||||
|
<span class="lang-en">{{ mineral.category }}</span>
|
||||||
|
<span class="lang-ru">{{ mineral.properties_ru.category|default:mineral.category }}</span>
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if mineral.crystal_system %}
|
{% if mineral.crystal_system %}
|
||||||
<div class="prop-item">
|
<div class="prop-item">
|
||||||
<span class="prop-label"><span class="lang-en">Crystal System</span><span class="lang-ru">Сингония</span></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>
|
<span class="prop-value">
|
||||||
|
<span class="lang-en">{{ mineral.crystal_system }}</span>
|
||||||
|
<span class="lang-ru">{{ mineral.properties_ru.crystal_system|default:mineral.crystal_system }}</span>
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if mineral.mohs_hardness %}
|
{% if mineral.mohs_hardness %}
|
||||||
@@ -572,19 +570,28 @@
|
|||||||
{% if mineral.luster %}
|
{% if mineral.luster %}
|
||||||
<div class="prop-item">
|
<div class="prop-item">
|
||||||
<span class="prop-label"><span class="lang-en">Luster</span><span class="lang-ru">Блеск</span></span>
|
<span class="prop-label"><span class="lang-en">Luster</span><span class="lang-ru">Блеск</span></span>
|
||||||
<span class="prop-value">{{ mineral.luster }}</span>
|
<span class="prop-value">
|
||||||
|
<span class="lang-en">{{ mineral.luster }}</span>
|
||||||
|
<span class="lang-ru">{{ mineral.properties_ru.luster|default:mineral.luster }}</span>
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if mineral.streak %}
|
{% if mineral.streak %}
|
||||||
<div class="prop-item">
|
<div class="prop-item">
|
||||||
<span class="prop-label"><span class="lang-en">Streak</span><span class="lang-ru">Черта</span></span>
|
<span class="prop-label"><span class="lang-en">Streak</span><span class="lang-ru">Черта</span></span>
|
||||||
<span class="prop-value">{{ mineral.streak }}</span>
|
<span class="prop-value">
|
||||||
|
<span class="lang-en">{{ mineral.streak }}</span>
|
||||||
|
<span class="lang-ru">{{ mineral.properties_ru.streak|default:mineral.streak }}</span>
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if mineral.specific_gravity %}
|
{% if mineral.specific_gravity %}
|
||||||
<div class="prop-item">
|
<div class="prop-item">
|
||||||
<span class="prop-label"><span class="lang-en">Specific Gravity</span><span class="lang-ru">Плотность</span></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>
|
<span class="prop-value">
|
||||||
|
<span class="lang-en">{{ mineral.specific_gravity }}</span>
|
||||||
|
<span class="lang-ru">{{ mineral.properties_ru.specific_gravity|default:mineral.specific_gravity }}</span>
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3,3 +3,4 @@ gunicorn>=21.0
|
|||||||
whitenoise>=6.6
|
whitenoise>=6.6
|
||||||
requests>=2.31
|
requests>=2.31
|
||||||
beautifulsoup4>=4.12
|
beautifulsoup4>=4.12
|
||||||
|
deep-translator>=1.11
|
||||||
|
|||||||
Reference in New Issue
Block a user