Features: 1) Add translate_fields management command for field translations via DeepL; 2) Add slug population script in 0022_category_slug migration.
Fixes: 1) Update locale codes in `deepl_translate.py` to lowercase for consistency. Extra: 1) Minor refactoring to ensure compatibility and avoid undefined behavior in slug population logic.
This commit is contained in:
parent
a6bbbc6101
commit
797e56a0cd
3 changed files with 162 additions and 17 deletions
|
|
@ -9,23 +9,23 @@ from django.core.management.base import BaseCommand, CommandError
|
|||
|
||||
# Mapping from Django locale codes to DeepL API codes
|
||||
DEEPL_TARGET_LANGUAGES_MAPPING = {
|
||||
"en-GB": "EN-GB",
|
||||
"ar-AR": "AR",
|
||||
"cs-CZ": "CS",
|
||||
"da-DK": "DA",
|
||||
"de-DE": "DE",
|
||||
"en-US": "EN-US",
|
||||
"es-ES": "ES",
|
||||
"fr-FR": "FR",
|
||||
"hi-IN": "unsupported",
|
||||
"it-IT": "IT",
|
||||
"ja-JP": "JA",
|
||||
"kk-KZ": "unsupported",
|
||||
"nl-NL": "NL",
|
||||
"pl-PL": "PL",
|
||||
"pt-BR": "PT-BR",
|
||||
"ro-RO": "RO",
|
||||
"ru-RU": "RU",
|
||||
"en-gb": "EN-GB",
|
||||
"ar-ar": "AR",
|
||||
"cs-cz": "CS",
|
||||
"da-dk": "DA",
|
||||
"de-de": "DE",
|
||||
"en-us": "EN-US",
|
||||
"es-es": "ES",
|
||||
"fr-fr": "FR",
|
||||
"hi-in": "unsupported",
|
||||
"it-it": "IT",
|
||||
"ja-jp": "JA",
|
||||
"kk-kz": "unsupported",
|
||||
"nl-nl": "NL",
|
||||
"pl-pl": "PL",
|
||||
"pt-br": "PT-BR",
|
||||
"ro-ro": "RO",
|
||||
"ru-ru": "RU",
|
||||
"zh-hans": "ZH-HANS",
|
||||
}
|
||||
|
||||
|
|
|
|||
137
core/management/commands/translate_fields.py
Normal file
137
core/management/commands/translate_fields.py
Normal file
|
|
@ -0,0 +1,137 @@
|
|||
import importlib
|
||||
import os
|
||||
|
||||
import requests
|
||||
from django.core.management.base import BaseCommand, CommandError
|
||||
from django.db import transaction
|
||||
|
||||
DEEPL_API_URL = "https://api-free.deepl.com/v2/translate"
|
||||
|
||||
DEEPL_TARGET_LANGUAGES_MAPPING = {
|
||||
"en-gb": "EN-GB",
|
||||
"ar-ar": "AR",
|
||||
"cs-cz": "CS",
|
||||
"da-dk": "DA",
|
||||
"de-de": "DE",
|
||||
"en-us": "EN-US",
|
||||
"es-es": "ES",
|
||||
"fr-fr": "FR",
|
||||
"hi-in": "unsupported",
|
||||
"it-it": "IT",
|
||||
"ja-jp": "JA",
|
||||
"kk-kz": "unsupported",
|
||||
"nl-nl": "NL",
|
||||
"pl-pl": "PL",
|
||||
"pt-br": "PT-BR",
|
||||
"ro-ro": "RO",
|
||||
"ru-ru": "RU",
|
||||
"zh-hans": "ZH-HANS",
|
||||
}
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = (
|
||||
"Translate a model field into another language via DeepL and store it "
|
||||
"in the translated_<lang> field created by django-modeltranslation."
|
||||
)
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument(
|
||||
"-t", "--target", required=True,
|
||||
help=(
|
||||
"Dotted path to the field to translate, "
|
||||
"e.g. core.models.Product.description"
|
||||
),
|
||||
)
|
||||
parser.add_argument(
|
||||
"-l", "--language", required=True,
|
||||
help=(
|
||||
"Modeltranslation language code to translate into, "
|
||||
"e.g. de-de, fr-fr, zh-hans"
|
||||
),
|
||||
)
|
||||
|
||||
def handle(self, *args, **options):
|
||||
target = options["target"]
|
||||
lang = options["language"].lower()
|
||||
|
||||
if lang not in DEEPL_TARGET_LANGUAGES_MAPPING:
|
||||
raise CommandError(f"Unknown language '{lang}'.")
|
||||
deepl_lang = DEEPL_TARGET_LANGUAGES_MAPPING[lang]
|
||||
if deepl_lang == "unsupported":
|
||||
raise CommandError(f"DeepL does not support translating into '{lang}'.")
|
||||
|
||||
try:
|
||||
module_path, model_name, field_name = target.rsplit(".", 2)
|
||||
except ValueError:
|
||||
raise CommandError(
|
||||
"Invalid target format. Use app.module.Model.field, e.g. core.models.Product.description"
|
||||
)
|
||||
|
||||
try:
|
||||
module = importlib.import_module(module_path)
|
||||
model = getattr(module, model_name)
|
||||
except (ImportError, AttributeError) as e:
|
||||
raise CommandError(f"Could not import model '{model_name}' from '{module_path}': {e}")
|
||||
|
||||
dest_suffix = lang.replace("-", "_")
|
||||
dest_field = f"{field_name}_{dest_suffix}"
|
||||
|
||||
if not hasattr(model, dest_field):
|
||||
raise CommandError(
|
||||
f"Model '{model_name}' has no field '{dest_field}'. "
|
||||
"Did you run makemigrations/migrate after setting up modeltranslation?"
|
||||
)
|
||||
|
||||
auth_key = os.environ.get("DEEPL_AUTH_KEY")
|
||||
if not auth_key:
|
||||
raise CommandError("Environment variable DEEPL_AUTH_KEY is not set.")
|
||||
|
||||
qs = model.objects.exclude(**{f"{field_name}__isnull": True}) \
|
||||
.exclude(**{f"{field_name}": ""})
|
||||
total = qs.count()
|
||||
if total == 0:
|
||||
self.stdout.write("No instances with non-empty source field found.")
|
||||
return
|
||||
|
||||
self.stdout.write(f"Translating {total} objects from '{field_name}' into '{dest_field}'.")
|
||||
|
||||
for obj in qs.iterator():
|
||||
src_text = getattr(obj, field_name)
|
||||
existing = getattr(obj, dest_field, None)
|
||||
if existing:
|
||||
self.stdout.write(f"Skipping {obj.pk}: '{dest_field}' already set.")
|
||||
continue
|
||||
|
||||
resp = requests.post(
|
||||
DEEPL_API_URL,
|
||||
data={
|
||||
"auth_key": auth_key,
|
||||
"text": src_text,
|
||||
"target_lang": deepl_lang,
|
||||
},
|
||||
timeout=30,
|
||||
)
|
||||
if resp.status_code != 200:
|
||||
self.stderr.write(
|
||||
f"DeepL API error for {obj.pk}: {resp.status_code} {resp.text}"
|
||||
)
|
||||
continue
|
||||
|
||||
data = resp.json()
|
||||
try:
|
||||
translated = data["translations"][0]["text"]
|
||||
except (KeyError, IndexError):
|
||||
self.stderr.write(f"Unexpected DeepL response for {obj.pk}: {data}")
|
||||
continue
|
||||
|
||||
setattr(obj, dest_field, translated)
|
||||
try:
|
||||
with transaction.atomic():
|
||||
obj.save(update_fields=[dest_field])
|
||||
except Exception as e:
|
||||
self.stderr.write(f"Error saving {obj.pk}: {e}")
|
||||
else:
|
||||
self.stdout.write(f"✓ {obj.pk}")
|
||||
|
||||
self.stdout.write(self.style.SUCCESS("Done."))
|
||||
|
|
@ -4,12 +4,20 @@ import django_extensions.db.fields
|
|||
from django.db import migrations
|
||||
|
||||
|
||||
def populate_slugs(apps, schema_editor):
|
||||
Category = apps.get_model('core', 'Category')
|
||||
for category in Category.objects.all():
|
||||
if not category.slug:
|
||||
category.save()
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
('core', '0021_rename_name_ar_ar_attribute_name_ar_ar_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(populate_slugs, reverse_code=migrations.RunPython.noop),
|
||||
migrations.AddField(
|
||||
model_name='category',
|
||||
name='slug',
|
||||
|
|
|
|||
Loading…
Reference in a new issue