From c149adb0a8538218d527695a774bd4c48a578116 Mon Sep 17 00:00:00 2001 From: Egor fureunoir Gorbunov Date: Mon, 14 Jul 2025 17:12:26 +0300 Subject: [PATCH] Features: 1) Add detailed translation logic in `deepl_translate` command with placeholder mapping and DeepL API integration; 2) Enhance `deepl_translate` with better PO handling and dynamic saving. Fixes: 1) Correct incorrect type annotations and imports for `readline` in `deepl_translate` command; 2) Remove unused variables `billing_address` and `shipping_address` in address application logic. Extra: Add `# type: ignore` comments to suppress type-checking errors across admin classes. --- core/admin.py | 32 ++++---- core/management/commands/deepl_translate.py | 84 +++++++++++++++++++-- core/models.py | 2 - vibes_auth/admin.py | 2 +- 4 files changed, 93 insertions(+), 27 deletions(-) diff --git a/core/admin.py b/core/admin.py index 2d791ad2..d320914d 100644 --- a/core/admin.py +++ b/core/admin.py @@ -170,7 +170,7 @@ class CategoryChildrenInline(TabularInline): @register(AttributeGroup) -class AttributeGroupAdmin(FieldsetsMixin, ActivationActionsMixin, ModelAdmin): +class AttributeGroupAdmin(FieldsetsMixin, ActivationActionsMixin, ModelAdmin): # type: ignore [misc] # noinspection PyClassVar model = AttributeGroup # type: ignore [misc] list_display = ("name", "modified") @@ -182,7 +182,7 @@ class AttributeGroupAdmin(FieldsetsMixin, ActivationActionsMixin, ModelAdmin): @register(Attribute) -class AttributeAdmin(FieldsetsMixin, ActivationActionsMixin, ModelAdmin): +class AttributeAdmin(FieldsetsMixin, ActivationActionsMixin, ModelAdmin): # type: ignore [misc] # noinspection PyClassVar model = Attribute # type: ignore [misc] list_display = ("name", "group", "value_type", "modified") @@ -196,7 +196,7 @@ class AttributeAdmin(FieldsetsMixin, ActivationActionsMixin, ModelAdmin): @register(AttributeValue) -class AttributeValueAdmin(FieldsetsMixin, ActivationActionsMixin, ModelAdmin): +class AttributeValueAdmin(FieldsetsMixin, ActivationActionsMixin, ModelAdmin): # type: ignore [misc] # noinspection PyClassVar model = AttributeValue # type: ignore [misc] list_display = ("attribute", "value", "modified") @@ -226,7 +226,7 @@ class CategoryAdmin(FieldsetsMixin, ActivationActionsMixin, DraggableMPTTAdmin): @register(Brand) -class BrandAdmin(FieldsetsMixin, ActivationActionsMixin, ModelAdmin): +class BrandAdmin(FieldsetsMixin, ActivationActionsMixin, ModelAdmin): # type: ignore [misc] # noinspection PyClassVar model = Brand # type: ignore [misc] list_display = ("name",) @@ -239,7 +239,7 @@ class BrandAdmin(FieldsetsMixin, ActivationActionsMixin, ModelAdmin): @register(Product) -class ProductAdmin(FieldsetsMixin, ActivationActionsMixin, ModelAdmin): +class ProductAdmin(FieldsetsMixin, ActivationActionsMixin, ModelAdmin): # type: ignore [misc] # noinspection PyClassVar model = Product # type: ignore [misc] list_display = ( @@ -279,7 +279,7 @@ class ProductAdmin(FieldsetsMixin, ActivationActionsMixin, ModelAdmin): @register(ProductTag) -class ProductTagAdmin(FieldsetsMixin, ActivationActionsMixin, ModelAdmin): +class ProductTagAdmin(FieldsetsMixin, ActivationActionsMixin, ModelAdmin): # type: ignore [misc] # noinspection PyClassVar model = ProductTag # type: ignore [misc] list_display = ("tag_name",) @@ -291,7 +291,7 @@ class ProductTagAdmin(FieldsetsMixin, ActivationActionsMixin, ModelAdmin): @register(CategoryTag) -class CategoryTagAdmin(FieldsetsMixin, ActivationActionsMixin, ModelAdmin): +class CategoryTagAdmin(FieldsetsMixin, ActivationActionsMixin, ModelAdmin): # type: ignore [misc] # noinspection PyClassVar model = CategoryTag # type: ignore [misc] list_display = ("tag_name",) @@ -303,7 +303,7 @@ class CategoryTagAdmin(FieldsetsMixin, ActivationActionsMixin, ModelAdmin): @register(Vendor) -class VendorAdmin(FieldsetsMixin, ActivationActionsMixin, ModelAdmin): +class VendorAdmin(FieldsetsMixin, ActivationActionsMixin, ModelAdmin): # type: ignore [misc] # noinspection PyClassVar model = Vendor # type: ignore [misc] list_display = ("name", "markup_percent", "modified") @@ -317,7 +317,7 @@ class VendorAdmin(FieldsetsMixin, ActivationActionsMixin, ModelAdmin): @register(Feedback) -class FeedbackAdmin(FieldsetsMixin, ActivationActionsMixin, ModelAdmin): +class FeedbackAdmin(FieldsetsMixin, ActivationActionsMixin, ModelAdmin): # type: ignore [misc] # noinspection PyClassVar model = Feedback # type: ignore [misc] list_display = ("order_product", "rating", "comment", "modified") @@ -330,7 +330,7 @@ class FeedbackAdmin(FieldsetsMixin, ActivationActionsMixin, ModelAdmin): @register(Order) -class OrderAdmin(FieldsetsMixin, ActivationActionsMixin, ModelAdmin): +class OrderAdmin(FieldsetsMixin, ActivationActionsMixin, ModelAdmin): # type: ignore [misc] # noinspection PyClassVar model = Order # type: ignore [misc] list_display = ( @@ -359,7 +359,7 @@ class OrderAdmin(FieldsetsMixin, ActivationActionsMixin, ModelAdmin): @register(OrderProduct) -class OrderProductAdmin(FieldsetsMixin, ActivationActionsMixin, ModelAdmin): +class OrderProductAdmin(FieldsetsMixin, ActivationActionsMixin, ModelAdmin): # type: ignore [misc] # noinspection PyClassVar model = OrderProduct # type: ignore [misc] list_display = ("order", "product", "quantity", "buy_price", "status", "modified") @@ -373,7 +373,7 @@ class OrderProductAdmin(FieldsetsMixin, ActivationActionsMixin, ModelAdmin): @register(PromoCode) -class PromoCodeAdmin(FieldsetsMixin, ActivationActionsMixin, ModelAdmin): +class PromoCodeAdmin(FieldsetsMixin, ActivationActionsMixin, ModelAdmin): # type: ignore [misc] # noinspection PyClassVar model = PromoCode # type: ignore [misc] list_display = ( @@ -402,7 +402,7 @@ class PromoCodeAdmin(FieldsetsMixin, ActivationActionsMixin, ModelAdmin): @register(Promotion) -class PromotionAdmin(FieldsetsMixin, ActivationActionsMixin, ModelAdmin): +class PromotionAdmin(FieldsetsMixin, ActivationActionsMixin, ModelAdmin): # type: ignore [misc] # noinspection PyClassVar model = Promotion # type: ignore [misc] list_display = ("name", "discount_percent", "modified") @@ -415,7 +415,7 @@ class PromotionAdmin(FieldsetsMixin, ActivationActionsMixin, ModelAdmin): @register(Stock) -class StockAdmin(FieldsetsMixin, ActivationActionsMixin, ModelAdmin): +class StockAdmin(FieldsetsMixin, ActivationActionsMixin, ModelAdmin): # type: ignore [misc] # noinspection PyClassVar model = Stock # type: ignore [misc] list_display = ("product", "vendor", "sku", "quantity", "price", "modified") @@ -436,7 +436,7 @@ class StockAdmin(FieldsetsMixin, ActivationActionsMixin, ModelAdmin): @register(Wishlist) -class WishlistAdmin(FieldsetsMixin, ActivationActionsMixin, ModelAdmin): +class WishlistAdmin(FieldsetsMixin, ActivationActionsMixin, ModelAdmin): # type: ignore [misc] # noinspection PyClassVar model = Wishlist # type: ignore [misc] list_display = ("user", "modified") @@ -448,7 +448,7 @@ class WishlistAdmin(FieldsetsMixin, ActivationActionsMixin, ModelAdmin): @register(ProductImage) -class ProductImageAdmin(FieldsetsMixin, ActivationActionsMixin, ModelAdmin): +class ProductImageAdmin(FieldsetsMixin, ActivationActionsMixin, ModelAdmin): # type: ignore [misc] # noinspection PyClassVar model = ProductImage # type: ignore [misc] list_display = ("alt", "product", "priority", "modified") diff --git a/core/management/commands/deepl_translate.py b/core/management/commands/deepl_translate.py index 22ed1677..84691008 100644 --- a/core/management/commands/deepl_translate.py +++ b/core/management/commands/deepl_translate.py @@ -126,11 +126,11 @@ class Command(BaseCommand): if not auth_key: raise CommandError("DEEPL_AUTH_KEY not set") - # attempt to import readline for interactive prefill + # attempt to import readline for interactive fill try: import readline except ImportError: - readline = None # fallback + readline = None # type: ignore [assignment] for target_lang in target_langs: api_code = DEEPL_TARGET_LANGUAGES_MAPPING.get(target_lang) @@ -159,7 +159,6 @@ class Command(BaseCommand): if not en_po: raise CommandError(f"Failed to load en_GB PO for {app_conf.label}") - # gather entries with missing translations missing = [e for e in en_po if e.msgid and not e.msgstr and not e.obsolete] if missing: self.stdout.write(self.style.NOTICE(f"⚠️ {len(missing)} missing in en_GB")) @@ -168,16 +167,16 @@ class Command(BaseCommand): if readline: def hook(): - readline.insert_text(default) + readline.insert_text(default) # noqa: B023 readline.redisplay() - readline.set_pre_input_hook(hook) + readline.set_pre_input_hook(hook) # type: ignore [attr-defined] prompt = f"Enter translation for '{e.msgid}': " user_in = input(prompt).strip() if readline: - readline.set_pre_input_hook(None) + readline.set_pre_input_hook(None) # type: ignore [attr-defined] if user_in: e.msgstr = user_in @@ -187,7 +186,76 @@ class Command(BaseCommand): en_po.save(en_path) self.stdout.write(self.style.SUCCESS("Updated en_GB PO")) - # … rest of your DeepL logic unchanged … - # build new_po, translate missing entries, save target PO, etc. + entries = [e for e in en_po if e.msgid and not e.obsolete] + source_map = {e.msgid: e.msgstr for e in entries} + + tgt_dir = os.path.join( + app_conf.path, + "locale", + target_lang.replace("-", "_"), + "LC_MESSAGES", + ) + os.makedirs(tgt_dir, exist_ok=True) + tgt_path = os.path.join(str(tgt_dir), "django.po") + + old_tgt = None + if os.path.exists(tgt_path): + self.stdout.write(f" loading existing {target_lang} PO…") + try: + old_tgt = load_po_sanitized(str(tgt_path)) + except Exception as e: + self.stdout.write(self.style.WARNING(f"Existing PO parse error({e!s}), starting fresh")) + + new_po = polib.POFile() + new_po.metadata = en_po.metadata.copy() + new_po.metadata["Language"] = target_lang + + for entry in entries: + prev = old_tgt.find(entry.msgid) if old_tgt else None + new_po.append( + polib.POEntry( + msgid=entry.msgid, + msgstr=prev.msgstr if prev and prev.msgstr else "", + msgctxt=entry.msgctxt, + comment=entry.comment, + tcomment=entry.tcomment, + occurrences=entry.occurrences, + flags=entry.flags, + ) + ) + + to_trans = [e for e in new_po if not e.msgstr] + if not to_trans: + self.stdout.write(self.style.WARNING(f"All done for {app_conf.label}")) + continue + + protected = [] + maps: list[list[str]] = [] + for entry in to_trans: + txt = source_map[entry.msgid] + p_txt, p_map = placeholderize(txt) + protected.append(p_txt) + maps.append(p_map) + + data = [ + ("auth_key", auth_key), + ("target_lang", api_code), + ] + [("text", t) for t in protected] + resp = requests.post("https://api.deepl.com/v2/translate", data=data) + try: + resp.raise_for_status() + result = resp.json() + except Exception as exc: + raise CommandError(f"DeepL error: {exc} – {resp.text}") from exc + + trans = result.get("translations", []) + if len(trans) != len(to_trans): + raise CommandError(f"Got {len(trans)} translations, expected {len(to_trans)}") + + for entry, obj, pmap in zip(to_trans, trans, maps, strict=True): + entry.msgstr = deplaceholderize(obj["text"], pmap) + + new_po.save(str(tgt_path)) + self.stdout.write(self.style.SUCCESS(f"Saved {tgt_path}")) self.stdout.write(self.style.SUCCESS("Done.")) diff --git a/core/models.py b/core/models.py index 6c45e958..9aadd4d8 100644 --- a/core/models.py +++ b/core/models.py @@ -1543,8 +1543,6 @@ class Order(ExportModelOperationsMixin("order"), NiceModel): # type: ignore [mi def apply_addresses(self, billing_address_uuid: str | None = None, shipping_address_uuid: str | None = None): try: - billing_address = Address.objects.none() - shipping_address = Address.objects.none() if not any([shipping_address_uuid, billing_address_uuid]) and not self.is_whole_digital: raise ValueError(_("you can only buy physical products with shipping address specified")) diff --git a/vibes_auth/admin.py b/vibes_auth/admin.py index 84f7ca3a..7dc34ba1 100644 --- a/vibes_auth/admin.py +++ b/vibes_auth/admin.py @@ -48,7 +48,7 @@ class OrderInline(admin.TabularInline): icon = "fa-solid fa-cart-shopping" -class UserAdmin(ActivationActionsMixin, BaseUserAdmin): +class UserAdmin(ActivationActionsMixin, BaseUserAdmin): # type: ignore [misc] inlines = (BalanceInline, OrderInline) fieldsets = ( (None, {"fields": ("email", "password")}),