Refactor translation command and improve order model.

Enhanced the `deepl_translate` management command with improved placeholder handling, error messages, and support for missing translations. Added `human_readable_id` and `is_business` attributes to the `Order` model, updating associated admin configurations to reflect these changes.
This commit is contained in:
Egor Pavlovich Gorbunov 2025-05-06 03:28:06 +03:00
parent 85a49c4e8b
commit 76d490f2e2
69 changed files with 5611 additions and 4967 deletions

View file

@ -263,12 +263,17 @@ class OrderProductInline(admin.TabularInline):
@admin.register(Order) @admin.register(Order)
class OrderAdmin(BasicModelAdmin): class OrderAdmin(BasicModelAdmin):
list_display = ("uuid", "user", "status", "total_price", "buy_time", "modified") list_display = ("human_readable_id", "user", "is_business", "status", "total_price", "buy_time", "modified")
list_filter = ("status", "buy_time", "modified", "created") list_filter = ("status", "buy_time", "modified", "created")
search_fields = ("user__email", "status") search_fields = ("user__email", "status", "uuid", "human_readable_id")
inlines = [OrderProductInline] inlines = [OrderProductInline]
form = OrderForm form = OrderForm
readonly_fields = ("total_price", "total_quantity", "buy_time") readonly_fields = ("total_price", "total_quantity", "buy_time", "human_readable_id")
def is_business(self, obj):
return obj.is_business
is_business.short_description = _("is business")
def get_queryset(self, request): def get_queryset(self, request):
qs = super().get_queryset(request) qs = super().get_queryset(request)

62
core/api_urls.py Normal file
View file

@ -0,0 +1,62 @@
from django.urls import include, path
from rest_framework.routers import DefaultRouter
from core.sitemaps import BrandSitemap, CategorySitemap, ProductSitemap
from core.views import (
CacheOperatorView,
ContactUsView,
GlobalSearchView,
RequestCursedURLView,
SupportedLanguagesView,
WebsiteParametersView,
download_digital_asset_view,
sitemap_detail,
sitemap_index,
)
from core.viewsets import (
AttributeGroupViewSet,
BrandViewSet,
CategoryViewSet,
FeedbackViewSet,
OrderViewSet,
ProductViewSet,
PromoCodeViewSet,
PromotionViewSet,
StockViewSet,
VendorViewSet,
WishlistViewSet,
)
core_router = DefaultRouter()
core_router.register(r"products", ProductViewSet, basename="products")
core_router.register(r"orders", OrderViewSet, basename="orders")
core_router.register(r"wishlists", WishlistViewSet, basename="wishlists")
core_router.register(r"attribute_groups", AttributeGroupViewSet, basename="attribute_groups")
core_router.register(r"brands", BrandViewSet, basename="brands")
core_router.register(r"categories", CategoryViewSet, basename="categories")
core_router.register(r"vendors", VendorViewSet, basename="vendors")
core_router.register(r"feedbacks", FeedbackViewSet, basename="feedbacks")
core_router.register(r"stocks", StockViewSet, basename="stocks")
core_router.register(r"promo_codes", PromoCodeViewSet, basename="promo_codes")
core_router.register(r"promotions", PromotionViewSet, basename="promotions")
sitemaps = {
'products': ProductSitemap,
'categories': CategorySitemap,
'brands': BrandSitemap,
}
urlpatterns = [
path("core/", include(core_router.urls)),
path("sitemap.xml", sitemap_index, {"sitemaps": sitemaps, "sitemap_url_name": "sitemap-detail"},
name="sitemap-index"),
path("sitemap-<section>.xml", sitemap_detail, {"sitemaps": sitemaps}, name="sitemap-detail"),
path("sitemap-<section>-<int:page>.xml", sitemap_detail, {"sitemaps": sitemaps}, name="sitemap-detail"),
path("download/<str:order_product_uuid>/", download_digital_asset_view, name="download_digital_asset"),
path("search/", GlobalSearchView.as_view(), name="global_search"),
path("app/cache/", CacheOperatorView.as_view(), name="cache_operator"),
path("app/languages/", SupportedLanguagesView.as_view(), name="supported_languages"),
path("app/parameters/", WebsiteParametersView.as_view(), name="parameters"),
path("app/contact_us/", ContactUsView.as_view(), name="contact_us"),
path("app/request_cursed_url/", RequestCursedURLView.as_view(), name="request_cursed_url"),
]

11
core/b2b_urls.py Normal file
View file

@ -0,0 +1,11 @@
from django.urls import path
from core.views import (
BuyAsBusinessView,
GlobalSearchView,
)
urlpatterns = [
path("search/", GlobalSearchView.as_view(), name="global_search"),
path("orders/buy_as_business/", BuyAsBusinessView.as_view(), name="request_cursed_url"),
]

View file

@ -1,6 +1,7 @@
{ {
"payment_methods": [ "payment_methods": [
"CASH", "CASH",
"CARD" "CARD",
"WIRE"
] ]
} }

View file

@ -8,7 +8,9 @@ from core.serializers import (
CacheOperatorSerializer, CacheOperatorSerializer,
ContactUsSerializer, ContactUsSerializer,
LanguageSerializer, LanguageSerializer,
BuyAsBusinessOrderSerializer,
) )
from payments.serializers import TransactionProcessSerializer
CACHE_SCHEMA = { CACHE_SCHEMA = {
"post": extend_schema( "post": extend_schema(
@ -82,3 +84,17 @@ SEARCH_SCHEMA = {
description=(_("global search endpoint to query across project's tables")), description=(_("global search endpoint to query across project's tables")),
) )
} }
BUY_AS_BUSINESS_SCHEMA = {
"post": extend_schema(
summary=_("purchase an order as a business"),
request=BuyAsBusinessOrderSerializer,
responses={
200: TransactionProcessSerializer,
400: error,
},
description=(
_("purchase an order as a business, using the provided `products` with `product_uuid` and `attributes`.")
),
)
}

View file

@ -196,7 +196,7 @@ class BuyOrder(BaseMutation):
class BuyUnregisteredOrder(BaseMutation): class BuyUnregisteredOrder(BaseMutation):
class Meta: class Meta:
description = _("buy an unregistered order") description = _("purchase an order without account creation")
class Arguments: class Arguments:
products = List(UUID, required=True) products = List(UUID, required=True)

View file

@ -280,6 +280,7 @@ class OrderType(DjangoObjectType):
"total_price", "total_price",
"total_quantity", "total_quantity",
"is_whole_digital", "is_whole_digital",
"human_readable_id",
) )
description = _("orders") description = _("orders")

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -1,12 +1,13 @@
import os import os
import re import re
import tempfile from tempfile import NamedTemporaryFile
import polib import polib
import requests import requests
from django.apps import apps from django.apps import apps
from django.core.management.base import BaseCommand, CommandError from django.core.management.base import BaseCommand, CommandError
# Mapping from Django locale codes to DeepL API codes
DEEPL_TARGET_LANGUAGES_MAPPING = { DEEPL_TARGET_LANGUAGES_MAPPING = {
"en-GB": "EN-GB", "en-GB": "EN-GB",
"ar-AR": "AR", "ar-AR": "AR",
@ -28,49 +29,72 @@ DEEPL_TARGET_LANGUAGES_MAPPING = {
"zh-hans": "ZH-HANS", "zh-hans": "ZH-HANS",
} }
# Patterns to identify placeholders
PLACEHOLDER_REGEXES = [
re.compile(r"\{[^}]+"), # {name}, {product_uuid}
re.compile(r"%\([^)]+\)[sd]"), # %(name)s, %(count)d
]
def load_po_sanitized(path): def placeholderize(text: str) -> tuple[str, list[str]]:
""" """
Attempt to load .po via polib; on OSError, normalize any '#,fuzzy' flags Replace placeholders with tokens and collect originals.
(inserting the missing space) and strip blank-header entries, then parse again. Returns (protected_text, placeholders_list).
"""
placeholders: list[str] = []
def _repl(match: re.Match) -> str:
idx = len(placeholders)
placeholders.append(match.group(0))
return f"__PH_{idx}__"
for rx in PLACEHOLDER_REGEXES:
text = rx.sub(_repl, text)
return text, placeholders
def deplaceholderize(text: str, placeholders: list[str]) -> str:
"""
Restore tokens back to original placeholders.
"""
for idx, ph in enumerate(placeholders):
text = text.replace(f"__PH_{idx}__", ph)
return text
def load_po_sanitized(path: str) -> polib.POFile | None:
"""
Load a .po file, sanitizing common issues if necessary.
""" """
try: try:
return polib.pofile(path) return polib.pofile(path)
except OSError: except OSError:
text = open(path, encoding="utf-8").read() with open(path, encoding="utf-8") as f:
# ensure any "#,fuzzy" becomes "#, fuzzy" so polib can parse flags text = f.read()
text = re.sub(r'^#,(?!\s)', '#, ', text, flags=re.MULTILINE) # fix malformed fuzzy flags
text = re.sub(r"^#,(?!\s)", "#, ", text, flags=re.MULTILINE)
# split off the header entry by the first blank line # remove empty header entries
parts = text.split("\n\n", 1) parts = text.split("\n\n", 1)
header = parts[0] header = parts[0]
rest = parts[1] if len(parts) > 1 else "" rest = parts[1] if len(parts) > 1 else ""
# drop any stray blank msgid/msgstr pairs that can also break parsing
rest_clean = re.sub( rest_clean = re.sub(
r'^msgid ""\s*\nmsgstr ""\s*\n?', "", r"^msgid \"\"\s*\nmsgstr \"\"\s*\n?", "",
rest, rest,
flags=re.MULTILINE flags=re.MULTILINE
) )
sanitized = header + "\n\n" + rest_clean sanitized = header + "\n\n" + rest_clean
tmp = NamedTemporaryFile(mode="w+", delete=False, suffix=".po", encoding="utf-8") # noqa: SIM115
# write to a temp file and parse
tmp = tempfile.NamedTemporaryFile(
mode="w+", delete=False, suffix=".po", encoding="utf-8"
)
tmp.write(sanitized)
tmp.flush()
tmp.close()
try: try:
po = polib.pofile(tmp.name) tmp.write(sanitized)
tmp.flush()
tmp.close()
return polib.pofile(tmp.name)
finally: finally:
os.unlink(tmp.name) try:
return po os.unlink(tmp.name)
except OSError as e:
raise CommandError("Failed to write sanitized .po file") from e
class Command(BaseCommand): class Command(BaseCommand):
help = "Merge msgid/msgstr from en_GB PO into target-language POs via DeepL" help = "Merge msgid/msgstr from en_GB PO into target-language POs via DeepL, preserving placeholders."
def add_arguments(self, parser): def add_arguments(self, parser):
parser.add_argument( parser.add_argument(
@ -79,7 +103,7 @@ class Command(BaseCommand):
action="append", action="append",
required=True, required=True,
metavar="LANG", metavar="LANG",
help="Locale code for translation, e.g. de-DE, fr-FR. Can be used multiple times." help="Locale code for translation, e.g. de-DE, fr-FR."
) )
parser.add_argument( parser.add_argument(
"-a", "--app", "-a", "--app",
@ -87,138 +111,110 @@ class Command(BaseCommand):
action="append", action="append",
required=True, required=True,
metavar="APP", metavar="APP",
help="Application name for translation, e.g. core, geo. Can be used multiple times." help="App label for translation, e.g. core, geo."
) )
def handle(self, *args, **options): def handle(self, *args, **options) -> None:
target_langs = options["target_languages"] target_langs: list[str] = options['target_languages']
target_apps = set(options["target_apps"]) target_apps: set[str] = set(options['target_apps'])
auth_key = os.environ.get("DEEPL_AUTH_KEY") auth_key = os.environ.get('DEEPL_AUTH_KEY')
if not auth_key: if not auth_key:
raise CommandError("Environment variable DEEPL_AUTH_KEY is not set.") raise CommandError('DEEPL_AUTH_KEY not set')
for target_lang in target_langs: for target_lang in target_langs:
api_code = DEEPL_TARGET_LANGUAGES_MAPPING.get(target_lang) api_code = DEEPL_TARGET_LANGUAGES_MAPPING.get(target_lang)
if not api_code: if not api_code:
self.stdout.write(self.style.WARNING(f"Ignoring unknown language '{target_lang}'")) self.stdout.write(self.style.WARNING(f"Unknown language '{target_lang}'"))
continue continue
if api_code == "unsupported": if api_code == 'unsupported':
self.stdout.write(self.style.WARNING(f"Skipping unsupported language '{target_lang}'")) self.stdout.write(self.style.WARNING(f"Unsupported language '{target_lang}'"))
continue continue
self.stdout.write(self.style.MIGRATE_HEADING( self.stdout.write(self.style.MIGRATE_HEADING(f"→ Translating into {target_lang}"))
f"→ Translating into {target_lang} (DeepL code: {api_code})"
))
for app_config in apps.get_app_configs(): for app_conf in apps.get_app_configs():
if app_config.label not in target_apps: if app_conf.label not in target_apps:
continue continue
# 1) load & sanitize English source .po en_path = os.path.join(app_conf.path, 'locale', 'en_GB', 'LC_MESSAGES', 'django.po')
en_po_path = os.path.join( if not os.path.isfile(en_path):
app_config.path, "locale", "en_GB", "LC_MESSAGES", "django.po" self.stdout.write(self.style.WARNING(f"{app_conf.label}: no en_GB PO"))
)
if not os.path.isfile(en_po_path):
self.stdout.write(self.style.WARNING(
f"{app_config.label}: no en_GB PO found, skipping"
))
continue continue
self.stdout.write(f" {app_config.label}: loading English PO…") self.stdout.write(f"{app_conf.label}: loading English PO…")
en_po = load_po_sanitized(en_po_path) en_po = load_po_sanitized(en_path)
# collect all non-obsolete entries with a msgid missing = [e for e in en_po if e.msgid and not e.msgstr and not e.obsolete]
en_entries = [ if missing:
e for e in en_po self.stdout.write(self.style.NOTICE(f"⚠️ {len(missing)} missing in en_GB"))
if e.msgid and not e.obsolete for e in missing:
] e.msgstr = input(f"Enter translation for '{e.msgid}': ").strip()
# map msgid -> source text (prefer existing msgstr if any) en_po.save(en_path)
source_texts = { self.stdout.write(self.style.SUCCESS("Updated en_GB PO"))
e.msgid: (e.msgstr or e.msgid)
for e in en_entries
}
# 2) load (or create) the target .po entries = [e for e in en_po if e.msgid and not e.obsolete]
tgt_po_dir = os.path.join( source_map = {e.msgid: e.msgstr for e in entries}
app_config.path,
"locale",
target_lang.replace("-", "_"),
"LC_MESSAGES"
)
os.makedirs(tgt_po_dir, exist_ok=True)
tgt_po_path = os.path.join(tgt_po_dir, "django.po")
if os.path.exists(tgt_po_path): tgt_dir = os.path.join(app_conf.path, 'locale', target_lang.replace('-', '_'), 'LC_MESSAGES')
self.stdout.write(f" loading existing {target_lang} PO…") os.makedirs(tgt_dir, exist_ok=True)
tgt_path = os.path.join(tgt_dir, 'django.po')
old_tgt = None
if os.path.exists(tgt_path):
self.stdout.write(f" loading existing {target_lang} PO…")
try: try:
old_tgt = load_po_sanitized(tgt_po_path) old_tgt = load_po_sanitized(tgt_path)
except Exception: except Exception as e:
self.stdout.write(self.style.WARNING( self.stdout.write(self.style.WARNING(f"Existing PO parse error({e!s}), starting fresh"))
" ! existing target PO parse error, starting fresh"
))
old_tgt = None
else:
old_tgt = None
# rebuild a new PO file in the same order as English
new_po = polib.POFile() new_po = polib.POFile()
new_po.metadata = en_po.metadata.copy() new_po.metadata = en_po.metadata.copy()
new_po.metadata["Language"] = target_lang new_po.metadata['Language'] = target_lang
for e in en_entries: for e in entries:
prev = old_tgt.find(e.msgid) if old_tgt else None prev = old_tgt.find(e.msgid) if old_tgt else None
entry = polib.POEntry( new_po.append(polib.POEntry(
msgid=e.msgid, msgid=e.msgid,
msgstr=(prev.msgstr if prev and prev.msgstr else ""), msgstr=prev.msgstr if prev and prev.msgstr else '',
msgctxt=e.msgctxt, msgctxt=e.msgctxt,
comment=e.comment, comment=e.comment,
tcomment=e.tcomment, tcomment=e.tcomment,
occurrences=e.occurrences, occurrences=e.occurrences,
flags=e.flags, flags=e.flags,
)
new_po.append(entry)
# 3) find which still need translating
to_translate = [e for e in new_po if not e.msgstr]
if not to_translate:
self.stdout.write(self.style.WARNING(
f" ! all entries already translated for {app_config.label}"
)) ))
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 continue
texts = [source_texts[e.msgid] for e in to_translate] protected = []
maps: list[list[str]] = []
for e in to_trans:
txt = source_map[e.msgid]
p_txt, p_map = placeholderize(txt)
protected.append(p_txt)
maps.append(p_map)
# 4) call DeepL data = [
resp = requests.post( ('auth_key', auth_key),
"https://api-free.deepl.com/v2/translate", ('target_lang', api_code),
data={ ] + [('text', t) for t in protected]
"auth_key": auth_key, resp = requests.post('https://api-free.deepl.com/v2/translate', data=data)
"target_lang": api_code,
"text": texts,
}
)
try: try:
resp.raise_for_status() resp.raise_for_status()
data = resp.json() result = resp.json()
except Exception as exc: except Exception as exc:
raise CommandError( raise CommandError(f"DeepL error: {exc} {resp.text}")
f"DeepL API error for {app_config.label}|{target_lang}: "
f"{exc} {resp.text}"
)
translations = data.get("translations", []) trans = result.get('translations', [])
if len(translations) != len(to_translate): if len(trans) != len(to_trans):
raise CommandError( raise CommandError(f"Got {len(trans)} translations, expected {len(to_trans)}")
f"Unexpected translations count: {len(translations)} vs {len(to_translate)}"
)
for entry, trans in zip(to_translate, translations): for e, obj, pmap in zip(to_trans, trans, maps):
entry.msgstr = trans["text"] e.msgstr = deplaceholderize(obj['text'], pmap)
# 5) save merged & translated PO new_po.save(tgt_path)
new_po.save(tgt_po_path) self.stdout.write(self.style.SUCCESS(f"Saved {tgt_path}"))
self.stdout.write(self.style.SUCCESS(
f" ✔ saved {target_lang} PO: {tgt_po_path}"
))
self.stdout.write(self.style.SUCCESS("All translations complete.")) self.stdout.write(self.style.SUCCESS("Done."))

View file

@ -0,0 +1,150 @@
import contextlib
import os
import re
from tempfile import NamedTemporaryFile
import polib
from django.apps import apps
from django.core.management.base import BaseCommand, CommandError
# Patterns to identify placeholders
PLACEHOLDER_REGEXES = [
re.compile(r"\{[^}]+\}"), # {name}, {type(instance)!s}, etc.
re.compile(r"%\([^)]+\)[sd]"), # %(verbose_name)s, %(count)d
]
def extract_placeholders(text: str) -> set[str]:
"""
Extract all placeholders from given text.
"""
phs: list[str] = []
for rx in PLACEHOLDER_REGEXES:
phs.extend(rx.findall(text))
return set(phs)
def load_po_sanitized(path: str) -> polib.POFile:
"""
Load a .po file via polib, sanitizing on parse errors.
Raises CommandError if still unparsable.
"""
try:
return polib.pofile(path)
except Exception:
# read raw text
try:
with open(path, encoding='utf-8') as f:
text = f.read()
except OSError as e:
raise CommandError(f"{path}: cannot read file ({e})")
# fix fuzzy flags and empty header entries
text = re.sub(r"^#,(?!\s)", "#, ", text, flags=re.MULTILINE)
parts = text.split("\n\n", 1)
header = parts[0]
rest = parts[1] if len(parts) > 1 else ''
rest = re.sub(r"^msgid \"\"\s*\nmsgstr \"\"\s*\n?", '', rest, flags=re.MULTILINE)
sanitized = header + "\n\n" + rest
tmp = NamedTemporaryFile(mode='w+', delete=False, suffix='.po', encoding='utf-8') # noqa: SIM115
try:
tmp.write(sanitized)
tmp.flush()
tmp.close()
return polib.pofile(tmp.name)
except Exception as e:
raise CommandError(f"{path}: syntax error after sanitization ({e})")
finally:
with contextlib.suppress(OSError):
os.unlink(tmp.name)
class Command(BaseCommand):
help = (
"Scan target-language .po files and report any placeholder mismatches, grouped by app."
)
def add_arguments(self, parser):
parser.add_argument(
'-l', '--language',
dest='target_languages',
action='append',
required=True,
metavar='LANG',
help='Locale code(s) to scan, e.g. de-DE, fr-FR'
)
parser.add_argument(
'-a', '--app',
dest='target_apps',
action='append',
required=True,
metavar='APP',
help='App label(s) to scan, e.g. core, geo'
)
parser.add_argument(
'-p', '--path',
dest='root_path',
required=False,
metavar='ROOT_PATH',
help='Root path prefix to adjust file links'
)
def handle(self, *args, **options) -> None:
langs: list[str] = options['target_languages']
apps_to_scan: set[str] = set(options['target_apps'])
root_path: str = options.get('root_path') or '/app/'
for app_conf in apps.get_app_configs():
if app_conf.label not in apps_to_scan:
continue
# Collect issues per app across all languages
app_issues: list[str] = []
for lang in langs:
loc = lang.replace('-', '_')
po_path = os.path.join(
app_conf.path, 'locale', loc, 'LC_MESSAGES', 'django.po'
)
if not os.path.exists(po_path):
continue
try:
po = load_po_sanitized(po_path)
except CommandError:
continue
# Collect lines for this language
lang_issues: list[str] = []
for entry in po:
if not entry.msgid or not entry.msgstr or entry.obsolete:
continue
src_ph = extract_placeholders(entry.msgid)
dst_ph = extract_placeholders(entry.msgstr)
missing = src_ph - dst_ph
extra = dst_ph - src_ph
if missing or extra:
line_no = entry.linenum or '?'
display = po_path.replace('/app/', root_path)
if '\\' in root_path:
display = display.replace('/', '\\')
lang_issues.append(
f" {display}:{line_no}: missing={sorted(missing)} extra={sorted(extra)}"
)
if lang_issues:
# Header for language with issues
app_issues.append(f"{lang}")
app_issues.extend(lang_issues)
# Output per app
if app_issues:
self.stdout.write(f"→ App: {app_conf.label}")
for line in app_issues:
self.stdout.write(line)
self.stdout.write("")
else:
# No issues in any language for this app
self.stdout.write(
self.style.SUCCESS(f"App {app_conf.label} has no placeholder issues.")
)
self.stdout.write("")
self.stdout.write(self.style.SUCCESS("Done scanning."))

View file

@ -40,7 +40,7 @@ from mptt.models import MPTTModel
from core.abstract import NiceModel from core.abstract import NiceModel
from core.choices import ORDER_PRODUCT_STATUS_CHOICES, ORDER_STATUS_CHOICES from core.choices import ORDER_PRODUCT_STATUS_CHOICES, ORDER_STATUS_CHOICES
from core.errors import NotEnoughMoneyError, DisabledCommerceError from core.errors import NotEnoughMoneyError, DisabledCommerceError
from core.utils import get_product_uuid_as_path, get_random_code from core.utils import get_product_uuid_as_path, get_random_code, generate_human_readable_id
from core.utils.lists import FAILED_STATUSES from core.utils.lists import FAILED_STATUSES
from core.validators import validate_category_image_dimensions from core.validators import validate_category_image_dimensions
from evibes.settings import CURRENCY_CODE from evibes.settings import CURRENCY_CODE
@ -493,6 +493,13 @@ class Order(NiceModel):
null=True, null=True,
blank=True, blank=True,
) )
human_readable_id = CharField(
max_length=6,
help_text=_("a human-readable identifier for the order"),
verbose_name=_("human readable id"),
unique=True,
default=generate_human_readable_id,
)
class Meta: class Meta:
verbose_name = _("order") verbose_name = _("order")
@ -501,6 +508,10 @@ class Order(NiceModel):
def __str__(self) -> str: def __str__(self) -> str:
return f"{self.pk} Order for {self.user.email}" return f"{self.pk} Order for {self.user.email}"
@property
def is_business(self) -> bool:
return self.attributes.get("is_business", False)
@property @property
def total_price(self) -> float: def total_price(self) -> float:
return ( return (

View file

@ -81,3 +81,13 @@ class BuyUnregisteredOrderSerializer(Serializer):
billing_customer_address = UnregisteredCustomerAddressSerializer(required=True) billing_customer_address = UnregisteredCustomerAddressSerializer(required=True)
shipping_customer_address = UnregisteredCustomerAddressSerializer(required=False) shipping_customer_address = UnregisteredCustomerAddressSerializer(required=False)
payment_method = CharField(required=True) payment_method = CharField(required=True)
class BuyAsBusinessOrderSerializer(Serializer):
products = ListField(child=AddOrderProductSerializer(), required=True)
business_inn = CharField(required=True)
business_email = CharField(required=True)
business_phone_number = CharField(required=True)
billing_business_address = UnregisteredCustomerAddressSerializer(required=True)
shipping_business_address = UnregisteredCustomerAddressSerializer(required=False)
payment_method = CharField(required=True)

View file

@ -411,6 +411,7 @@ class OrderDetailSerializer(ModelSerializer):
"shipping_address", "shipping_address",
"buy_time", "buy_time",
"order_products", "order_products",
"human_readable_id",
"created", "created",
"modified", "modified",
] ]

View file

@ -291,6 +291,7 @@ class OrderSimpleSerializer(ModelSerializer):
model = Order model = Order
fields = [ fields = [
"uuid", "uuid",
"human_readable_id",
"status", "status",
"user", "user",
"promo_code", "promo_code",

View file

@ -1,62 +1 @@
from django.urls import include, path urlpatterns = []
from rest_framework.routers import DefaultRouter
from core.sitemaps import BrandSitemap, CategorySitemap, ProductSitemap
from core.views import (
CacheOperatorView,
ContactUsView,
GlobalSearchView,
RequestCursedURLView,
SupportedLanguagesView,
WebsiteParametersView,
download_digital_asset_view,
sitemap_detail,
sitemap_index,
)
from core.viewsets import (
AttributeGroupViewSet,
BrandViewSet,
CategoryViewSet,
FeedbackViewSet,
OrderViewSet,
ProductViewSet,
PromoCodeViewSet,
PromotionViewSet,
StockViewSet,
VendorViewSet,
WishlistViewSet,
)
core_router = DefaultRouter()
core_router.register(r"products", ProductViewSet, basename="products")
core_router.register(r"orders", OrderViewSet, basename="orders")
core_router.register(r"wishlists", WishlistViewSet, basename="wishlists")
core_router.register(r"attribute_groups", AttributeGroupViewSet, basename="attribute_groups")
core_router.register(r"brands", BrandViewSet, basename="brands")
core_router.register(r"categories", CategoryViewSet, basename="categories")
core_router.register(r"vendors", VendorViewSet, basename="vendors")
core_router.register(r"feedbacks", FeedbackViewSet, basename="feedbacks")
core_router.register(r"stocks", StockViewSet, basename="stocks")
core_router.register(r"promo_codes", PromoCodeViewSet, basename="promo_codes")
core_router.register(r"promotions", PromotionViewSet, basename="promotions")
sitemaps = {
'products': ProductSitemap,
'categories': CategorySitemap,
'brands': BrandSitemap,
}
urlpatterns = [
path("core/", include(core_router.urls)),
path("sitemap.xml", sitemap_index, {"sitemaps": sitemaps, "sitemap_url_name": "sitemap-detail"},
name="sitemap-index"),
path("sitemap-<section>.xml", sitemap_detail, {"sitemaps": sitemaps}, name="sitemap-detail"),
path("sitemap-<section>-<int:page>.xml", sitemap_detail, {"sitemaps": sitemaps}, name="sitemap-detail"),
path("download/<str:order_product_uuid>/", download_digital_asset_view, name="download_digital_asset"),
path("search/", GlobalSearchView.as_view(), name="global_search"),
path("app/cache/", CacheOperatorView.as_view(), name="cache_operator"),
path("app/languages/", SupportedLanguagesView.as_view(), name="supported_languages"),
path("app/parameters/", WebsiteParametersView.as_view(), name="parameters"),
path("app/contact_us/", ContactUsView.as_view(), name="contact_us"),
path("app/request_cursed_url/", RequestCursedURLView.as_view(), name="request_cursed_url"),
]

View file

@ -1,5 +1,6 @@
import logging import logging
import re import re
import secrets
from contextlib import contextmanager from contextlib import contextmanager
from constance import config from constance import config
@ -126,3 +127,21 @@ def resolve_translations_for_elasticsearch(instance, field_name):
filled_field = getattr(instance, field_name, "") filled_field = getattr(instance, field_name, "")
if not field: if not field:
setattr(instance, f"{field_name}_{LANGUAGE_CODE}", filled_field) setattr(instance, f"{field_name}_{LANGUAGE_CODE}", filled_field)
CROCKFORD = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789"
def generate_human_readable_id(length: int = 6) -> str:
"""
Generate a human-readable ID of `length` characters (from the Crockford set),
with a single hyphen inserted:
- 50% chance at the exact middle
- 50% chance at a random position between characters (1 to length-1)
The final string length will be `length + 1` (including the hyphen).
"""
chars = [secrets.choice(CROCKFORD) for _ in range(length)]
pos = (secrets.randbelow(length - 1) + 1) if secrets.choice([True, False]) else (length // 2)
chars.insert(pos, "-")
return "".join(chars)

View file

@ -33,13 +33,15 @@ from core.docs.drf.views import (
PARAMETERS_SCHEMA, PARAMETERS_SCHEMA,
REQUEST_CURSED_URL_SCHEMA, REQUEST_CURSED_URL_SCHEMA,
SEARCH_SCHEMA, SEARCH_SCHEMA,
BUY_AS_BUSINESS_SCHEMA,
) )
from core.elasticsearch import process_query from core.elasticsearch import process_query
from core.models import DigitalAssetDownload from core.models import DigitalAssetDownload, Order
from core.serializers import ( from core.serializers import (
CacheOperatorSerializer, CacheOperatorSerializer,
ContactUsSerializer, ContactUsSerializer,
LanguageSerializer, LanguageSerializer,
BuyAsBusinessOrderSerializer,
) )
from core.utils import get_project_parameters, is_url_safe from core.utils import get_project_parameters, is_url_safe
from core.utils.caching import web_cache from core.utils.caching import web_cache
@ -47,17 +49,18 @@ from core.utils.emailing import contact_us_email
from core.utils.languages import get_flag_by_language from core.utils.languages import get_flag_by_language
from evibes import settings from evibes import settings
from evibes.settings import LANGUAGES from evibes.settings import LANGUAGES
from payments.serializers import TransactionProcessSerializer
def sitemap_index(request, *args, **kwargs): def sitemap_index(request, *args, **kwargs):
response = _sitemap_index_view(request, *args, **kwargs) response = _sitemap_index_view(request, *args, **kwargs)
response['Content-Type'] = 'application/xml; charset=utf-8' response["Content-Type"] = "application/xml; charset=utf-8"
return response return response
def sitemap_detail(request, *args, **kwargs): def sitemap_detail(request, *args, **kwargs):
response = _sitemap_detail_view(request, *args, **kwargs) response = _sitemap_detail_view(request, *args, **kwargs)
response['Content-Type'] = 'application/xml; charset=utf-8' response["Content-Type"] = "application/xml; charset=utf-8"
return response return response
@ -190,12 +193,34 @@ class GlobalSearchView(APIView):
A global search endpoint. A global search endpoint.
It returns a response grouping matched items by index. It returns a response grouping matched items by index.
""" """
renderer_classes = [CamelCaseJSONRenderer, MultiPartRenderer, XMLRenderer, YAMLRenderer] renderer_classes = [CamelCaseJSONRenderer, MultiPartRenderer, XMLRenderer, YAMLRenderer]
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
return Response(camelize({"results": process_query(request.GET.get("q", "").strip())})) return Response(camelize({"results": process_query(request.GET.get("q", "").strip())}))
@extend_schema_view(**BUY_AS_BUSINESS_SCHEMA)
class BuyAsBusinessView(APIView):
@ratelimit(key="ip", rate="2/h", block=True)
def post(self, request, *_args, **kwargs):
serializer = BuyAsBusinessOrderSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
order = Order.objects.create(status="MOMENTAL")
products = [product.get("product_uuid") for product in serializer.validated_data.get("products")]
transaction = order.buy_without_registration(
products=products,
promocode_uuid=serializer.validated_data.get("promocode_uuid"),
customer_name=serializer.validated_data.get("customer_name"),
customer_email=serializer.validated_data.get("customer_email"),
customer_phone=serializer.validated_data.get("customer_phone"),
customer_billing_address=serializer.validated_data.get("customer_billing_address"),
customer_shipping_address=serializer.validated_data.get("customer_shipping_address"),
payment_method=serializer.validated_data.get("payment_method"),
)
return Response(status=status.HTTP_202_ACCEPTED, data=TransactionProcessSerializer(transaction).data)
def download_digital_asset_view(request, *args, **kwargs): def download_digital_asset_view(request, *args, **kwargs):
try: try:
uuid = force_str(urlsafe_base64_decode(kwargs["encoded_uuid"])) uuid = force_str(urlsafe_base64_decode(kwargs["encoded_uuid"]))

View file

@ -73,6 +73,7 @@ from core.serializers import (
VendorSimpleSerializer, VendorSimpleSerializer,
WishlistDetailSerializer, WishlistDetailSerializer,
WishlistSimpleSerializer, WishlistSimpleSerializer,
BuyAsBusinessOrderSerializer,
) )
from core.utils import format_attributes from core.utils import format_attributes
from core.utils.messages import permission_denied_message from core.utils.messages import permission_denied_message
@ -228,16 +229,16 @@ class OrderViewSet(EvibesViewSet):
serializer.is_valid(raise_exception=True) serializer.is_valid(raise_exception=True)
order = Order.objects.create(status="MOMENTAL") order = Order.objects.create(status="MOMENTAL")
products = [product.get("product_uuid") for product in serializer.validated_data.get("products")] products = [product.get("product_uuid") for product in serializer.validated_data.get("products")]
transaction = order.buy_without_registration(products=products, transaction = order.buy_without_registration(
promocode_uuid=serializer.validated_data.get("promocode_uuid"), products=products,
customer_name=serializer.validated_data.get("customer_name"), promocode_uuid=serializer.validated_data.get("promocode_uuid"),
customer_email=serializer.validated_data.get("customer_email"), customer_name=serializer.validated_data.get("customer_name"),
customer_phone=serializer.validated_data.get("customer_phone"), customer_email=serializer.validated_data.get("customer_email"),
customer_billing_address=serializer.validated_data.get( customer_phone=serializer.validated_data.get("customer_phone"),
"customer_billing_address"), customer_billing_address=serializer.validated_data.get("customer_billing_address"),
customer_shipping_address=serializer.validated_data.get( customer_shipping_address=serializer.validated_data.get("customer_shipping_address"),
"customer_shipping_address"), payment_method=serializer.validated_data.get("payment_method"),
payment_method=serializer.validated_data.get("payment_method")) )
return Response(status=status.HTTP_202_ACCEPTED, data=TransactionProcessSerializer(transaction).data) return Response(status=status.HTTP_202_ACCEPTED, data=TransactionProcessSerializer(transaction).data)
@action(detail=True, methods=["post"], url_path="add_order_product") @action(detail=True, methods=["post"], url_path="add_order_product")

View file

@ -22,7 +22,7 @@ urlpatterns = [
path(r"i18n/", include("django.conf.urls.i18n")), path(r"i18n/", include("django.conf.urls.i18n")),
path(r"favicon.ico", favicon_view), path(r"favicon.ico", favicon_view),
path(r"", index), path(r"", index),
path(r"", include("core.urls")), path(r"", include("core.api_urls")),
path(r"auth/", include("vibes_auth.urls")), path(r"auth/", include("vibes_auth.urls")),
path(r"geo/", include("geo.urls")), path(r"geo/", include("geo.urls")),
path(r"payments/", include("payments.urls")), path(r"payments/", include("payments.urls")),

View file

@ -3,7 +3,7 @@
# This file is distributed under the same license as the eVibes package. # This file is distributed under the same license as the eVibes package.
# EGOR GORBUNOV <CONTACT@FUREUNOIR.COM>, 2025. # EGOR GORBUNOV <CONTACT@FUREUNOIR.COM>, 2025.
# #
#,fuzzy #, fuzzy
msgid "" msgid ""
msgstr "" msgstr ""
"Project-Id-Version: 1\n" "Project-Id-Version: 1\n"

View file

@ -3,7 +3,7 @@
# This file is distributed under the same license as the eVibes package. # This file is distributed under the same license as the eVibes package.
# EGOR GORBUNOV <CONTACT@FUREUNOIR.COM>, 2025. # EGOR GORBUNOV <CONTACT@FUREUNOIR.COM>, 2025.
# #
#,fuzzy #, fuzzy
msgid "" msgid ""
msgstr "" msgstr ""
"Project-Id-Version: 1\n" "Project-Id-Version: 1\n"

View file

@ -14,5 +14,8 @@ def create_balance_on_user_creation_signal(instance, created, **kwargs):
@receiver(post_save, sender=Transaction) @receiver(post_save, sender=Transaction)
def process_transaction_changes(instance, created, **kwargs): def process_transaction_changes(instance, created, **kwargs):
if created: if created:
gateway = object() try:
gateway.process_transaction(instance) gateway = object()
gateway.process_transaction(instance)
except Exception: # noqa:
instance.process = {"status": "NOGATEWAY"}

View file

@ -60,7 +60,7 @@ msgstr ""
#: vibes_auth/graphene/mutations.py:103 #: vibes_auth/graphene/mutations.py:103
#, python-brace-format #, python-brace-format
msgid "{name} does not exist: {uuid}" msgid "{name} does not exist: {uuid}"
msgstr "{Name} existiert nicht: {uuid}" msgstr "{name} existiert nicht: {uuid}"
#: vibes_auth/graphene/mutations.py:111 #: vibes_auth/graphene/mutations.py:111
msgid "malformed email" msgid "malformed email"
@ -74,7 +74,7 @@ msgstr "Fehlerhafte Telefonnummer: {phone_number}"
#: vibes_auth/graphene/mutations.py:134 #: vibes_auth/graphene/mutations.py:134
#, python-brace-format #, python-brace-format
msgid "Invalid attribute format: {attribute_pair}" msgid "Invalid attribute format: {attribute_pair}"
msgstr "Ungültiges Attributformat: {Attribut_paar}" msgstr "Ungültiges Attributformat: {attribute_pair}"
#: vibes_auth/graphene/mutations.py:261 #: vibes_auth/graphene/mutations.py:261
msgid "activation link is invalid!" msgid "activation link is invalid!"
@ -282,7 +282,7 @@ msgstr ""
#: vibes_auth/templates/user_reset_password_email.html:89 #: vibes_auth/templates/user_reset_password_email.html:89
#, python-format #, python-format
msgid "best regards,<br>The %(project_name)s team" msgid "best regards,<br>The %(project_name)s team"
msgstr "Mit freundlichen Grüßen,<br>Das Team von %(Projektname)" msgstr "Mit freundlichen Grüßen,<br>Das Team von %(project_name)s"
#: vibes_auth/templates/user_reset_password_email.html:95 #: vibes_auth/templates/user_reset_password_email.html:95
#: vibes_auth/templates/user_verification_email.html:107 #: vibes_auth/templates/user_verification_email.html:107
@ -300,7 +300,7 @@ msgid ""
"thank you for signing up for %(project_name)s. please activate your account " "thank you for signing up for %(project_name)s. please activate your account "
"by clicking the button below:" "by clicking the button below:"
msgstr "" msgstr ""
"Vielen Dank, dass Sie sich für %(Projektname)s angemeldet haben. Bitte " "Vielen Dank, dass Sie sich für %(project_name)s angemeldet haben. Bitte "
"aktivieren Sie Ihr Konto, indem Sie auf die Schaltfläche unten klicken:" "aktivieren Sie Ihr Konto, indem Sie auf die Schaltfläche unten klicken:"
#: vibes_auth/templates/user_verification_email.html:95 #: vibes_auth/templates/user_verification_email.html:95
@ -323,7 +323,7 @@ msgstr ""
#: vibes_auth/templates/user_verification_email.html:101 #: vibes_auth/templates/user_verification_email.html:101
#, python-format #, python-format
msgid "best regards,<br>the %(project_name)s team" msgid "best regards,<br>the %(project_name)s team"
msgstr "Mit freundlichen Grüßen,<br>das %(Projektname)s Team" msgstr "Mit freundlichen Grüßen,<br>das %(project_name)s Team"
#: vibes_auth/utils/emailing.py:25 #: vibes_auth/utils/emailing.py:25
#, python-brace-format #, python-brace-format

View file

@ -59,7 +59,7 @@ msgstr ""
#: vibes_auth/graphene/mutations.py:103 #: vibes_auth/graphene/mutations.py:103
#, python-brace-format #, python-brace-format
msgid "{name} does not exist: {uuid}" msgid "{name} does not exist: {uuid}"
msgstr "{nombre} no existe: {uuid}" msgstr "{name} no existe: {uuid}"
#: vibes_auth/graphene/mutations.py:111 #: vibes_auth/graphene/mutations.py:111
msgid "malformed email" msgid "malformed email"
@ -73,7 +73,7 @@ msgstr "Número de teléfono malformado: {phone_number}"
#: vibes_auth/graphene/mutations.py:134 #: vibes_auth/graphene/mutations.py:134
#, python-brace-format #, python-brace-format
msgid "Invalid attribute format: {attribute_pair}" msgid "Invalid attribute format: {attribute_pair}"
msgstr "Formato de atributo no válido: {par_atributo}" msgstr "Formato de atributo no válido: {attribute_pair}"
#: vibes_auth/graphene/mutations.py:261 #: vibes_auth/graphene/mutations.py:261
msgid "activation link is invalid!" msgid "activation link is invalid!"
@ -85,7 +85,7 @@ msgstr "La cuenta ya ha sido activada..."
#: vibes_auth/graphene/mutations.py:271 vibes_auth/graphene/mutations.py:321 #: vibes_auth/graphene/mutations.py:271 vibes_auth/graphene/mutations.py:321
msgid "something went wrong: {e!s}" msgid "something went wrong: {e!s}"
msgstr "Algo salió mal." msgstr "Algo salió mal: {e!s}."
#: vibes_auth/graphene/mutations.py:305 #: vibes_auth/graphene/mutations.py:305
msgid "passwords do not match" msgid "passwords do not match"
@ -249,7 +249,7 @@ msgstr "Confirmación de restablecimiento de contraseña"
#: vibes_auth/templates/user_verification_email.html:91 #: vibes_auth/templates/user_verification_email.html:91
#, python-format #, python-format
msgid "hello %(user_first_name)s," msgid "hello %(user_first_name)s,"
msgstr "Hola %(nombre_usuario)s," msgstr "Hola %(user_first_name)s,"
#: vibes_auth/templates/user_reset_password_email.html:82 #: vibes_auth/templates/user_reset_password_email.html:82
msgid "" msgid ""
@ -278,7 +278,7 @@ msgstr "Si no ha enviado esta solicitud, ignore este correo electrónico."
#: vibes_auth/templates/user_reset_password_email.html:89 #: vibes_auth/templates/user_reset_password_email.html:89
#, python-format #, python-format
msgid "best regards,<br>The %(project_name)s team" msgid "best regards,<br>The %(project_name)s team"
msgstr "Saludos cordiales,<br>El equipo de %(nombre_del_proyecto)s" msgstr "Saludos cordiales,<br>El equipo de %(project_name)s"
#: vibes_auth/templates/user_reset_password_email.html:95 #: vibes_auth/templates/user_reset_password_email.html:95
#: vibes_auth/templates/user_verification_email.html:107 #: vibes_auth/templates/user_verification_email.html:107
@ -296,7 +296,7 @@ msgid ""
"thank you for signing up for %(project_name)s. please activate your account " "thank you for signing up for %(project_name)s. please activate your account "
"by clicking the button below:" "by clicking the button below:"
msgstr "" msgstr ""
"Gracias por registrarse en %(nombre_del_proyecto)s. Por favor, active su " "Gracias por registrarse en %(project_name)s. Por favor, active su "
"cuenta haciendo clic en el botón de abajo:" "cuenta haciendo clic en el botón de abajo:"
#: vibes_auth/templates/user_verification_email.html:95 #: vibes_auth/templates/user_verification_email.html:95
@ -318,7 +318,7 @@ msgstr ""
#: vibes_auth/templates/user_verification_email.html:101 #: vibes_auth/templates/user_verification_email.html:101
#, python-format #, python-format
msgid "best regards,<br>the %(project_name)s team" msgid "best regards,<br>the %(project_name)s team"
msgstr "Saludos cordiales,<br>el equipo de %(nombre_del_proyecto)s" msgstr "Saludos cordiales,<br>el equipo de %(project_name)s"
#: vibes_auth/utils/emailing.py:25 #: vibes_auth/utils/emailing.py:25
#, python-brace-format #, python-brace-format
@ -332,7 +332,7 @@ msgstr "Usuario no encontrado con el UUID dado: {user_pk}"
#: vibes_auth/utils/emailing.py:49 vibes_auth/utils/emailing.py:90 #: vibes_auth/utils/emailing.py:49 vibes_auth/utils/emailing.py:90
msgid "something went wrong while sending an email: {e!s}" msgid "something went wrong while sending an email: {e!s}"
msgstr "Algo salió mal al enviar un correo electrónico: ." msgstr "Algo salió mal al enviar un correo electrónico: {e!s}."
#: vibes_auth/utils/emailing.py:65 #: vibes_auth/utils/emailing.py:65
#, python-brace-format #, python-brace-format

View file

@ -72,7 +72,7 @@ msgstr "不正な電話番号:{phone_number}。"
#: vibes_auth/graphene/mutations.py:134 #: vibes_auth/graphene/mutations.py:134
#, python-brace-format #, python-brace-format
msgid "Invalid attribute format: {attribute_pair}" msgid "Invalid attribute format: {attribute_pair}"
msgstr "無効な属性形式です:{属性ペア}。" msgstr "無効な属性形式です:{attribute_pair}。"
#: vibes_auth/graphene/mutations.py:261 #: vibes_auth/graphene/mutations.py:261
msgid "activation link is invalid!" msgid "activation link is invalid!"
@ -84,7 +84,7 @@ msgstr "アカウントはすでに有効になっています..."
#: vibes_auth/graphene/mutations.py:271 vibes_auth/graphene/mutations.py:321 #: vibes_auth/graphene/mutations.py:271 vibes_auth/graphene/mutations.py:321
msgid "something went wrong: {e!s}" msgid "something went wrong: {e!s}"
msgstr "何かが間違っていた:{e!" msgstr "何かが間違っていた:{e!s}"
#: vibes_auth/graphene/mutations.py:305 #: vibes_auth/graphene/mutations.py:305
msgid "passwords do not match" msgid "passwords do not match"
@ -277,7 +277,7 @@ msgstr "このリクエストを送信していない場合は、このメール
#: vibes_auth/templates/user_reset_password_email.html:89 #: vibes_auth/templates/user_reset_password_email.html:89
#, python-format #, python-format
msgid "best regards,<br>The %(project_name)s team" msgid "best regards,<br>The %(project_name)s team"
msgstr "よろしくお願いします、<br>%(プロジェクト名)のチーム" msgstr "よろしくお願いします、<br>%(project_name)sのチーム"
#: vibes_auth/templates/user_reset_password_email.html:95 #: vibes_auth/templates/user_reset_password_email.html:95
#: vibes_auth/templates/user_verification_email.html:107 #: vibes_auth/templates/user_verification_email.html:107
@ -295,7 +295,7 @@ msgid ""
"thank you for signing up for %(project_name)s. please activate your account " "thank you for signing up for %(project_name)s. please activate your account "
"by clicking the button below:" "by clicking the button below:"
msgstr "" msgstr ""
"(project_name)sにご登録いただきありがとうございます。下のボタンをクリックして" "%(project_name)sにご登録いただきありがとうございます。下のボタンをクリックして"
"アカウントを有効にしてください:" "アカウントを有効にしてください:"
#: vibes_auth/templates/user_verification_email.html:95 #: vibes_auth/templates/user_verification_email.html:95
@ -318,12 +318,12 @@ msgstr ""
#: vibes_auth/templates/user_verification_email.html:101 #: vibes_auth/templates/user_verification_email.html:101
#, python-format #, python-format
msgid "best regards,<br>the %(project_name)s team" msgid "best regards,<br>the %(project_name)s team"
msgstr "よろしくお願いします、<br>%(プロジェクト名)のチーム。" msgstr "よろしくお願いします、<br>%(project_name)sのチーム。"
#: vibes_auth/utils/emailing.py:25 #: vibes_auth/utils/emailing.py:25
#, python-brace-format #, python-brace-format
msgid "{config.PROJECT_NAME} | Activate Account" msgid "{config.PROJECT_NAME} | Activate Account"
msgstr "{コンフィグ.PROJECT_NAME}|アカウントの有効化| アカウントの有効化" msgstr "{config.PROJECT_NAME}|アカウントの有効化| アカウントの有効化"
#: vibes_auth/utils/emailing.py:46 vibes_auth/utils/emailing.py:87 #: vibes_auth/utils/emailing.py:46 vibes_auth/utils/emailing.py:87
#, python-brace-format #, python-brace-format
@ -332,12 +332,12 @@ msgstr "指定されたUUIDを持つユーザーが見つかりません{user
#: vibes_auth/utils/emailing.py:49 vibes_auth/utils/emailing.py:90 #: vibes_auth/utils/emailing.py:49 vibes_auth/utils/emailing.py:90
msgid "something went wrong while sending an email: {e!s}" msgid "something went wrong while sending an email: {e!s}"
msgstr "メール送信中に何か問題が発生しました:{e!" msgstr "メール送信中に何か問題が発生しました:{e!s}"
#: vibes_auth/utils/emailing.py:65 #: vibes_auth/utils/emailing.py:65
#, python-brace-format #, python-brace-format
msgid "{config.PROJECT_NAME} | Reset Password" msgid "{config.PROJECT_NAME} | Reset Password"
msgstr "{コンフィグ.PROJECT_NAME}。| パスワードのリセット" msgstr "{config.PROJECT_NAME}。| パスワードのリセット"
#: vibes_auth/validators.py:13 #: vibes_auth/validators.py:13
msgid "" msgid ""

View file

@ -60,7 +60,7 @@ msgstr ""
#: vibes_auth/graphene/mutations.py:103 #: vibes_auth/graphene/mutations.py:103
#, python-brace-format #, python-brace-format
msgid "{name} does not exist: {uuid}" msgid "{name} does not exist: {uuid}"
msgstr "{naam} bestaat niet: {uuid}" msgstr "{name} bestaat niet: {uuid}"
#: vibes_auth/graphene/mutations.py:111 #: vibes_auth/graphene/mutations.py:111
msgid "malformed email" msgid "malformed email"
@ -74,7 +74,7 @@ msgstr "Misvormd telefoonnummer: {phone_number}"
#: vibes_auth/graphene/mutations.py:134 #: vibes_auth/graphene/mutations.py:134
#, python-brace-format #, python-brace-format
msgid "Invalid attribute format: {attribute_pair}" msgid "Invalid attribute format: {attribute_pair}"
msgstr "Ongeldig attribuutformaat: {attribuut_paar}" msgstr "Ongeldig attribuutformaat: {attribute_pair}"
#: vibes_auth/graphene/mutations.py:261 #: vibes_auth/graphene/mutations.py:261
msgid "activation link is invalid!" msgid "activation link is invalid!"
@ -86,7 +86,7 @@ msgstr "Account is al geactiveerd..."
#: vibes_auth/graphene/mutations.py:271 vibes_auth/graphene/mutations.py:321 #: vibes_auth/graphene/mutations.py:271 vibes_auth/graphene/mutations.py:321
msgid "something went wrong: {e!s}" msgid "something went wrong: {e!s}"
msgstr "Er ging iets mis." msgstr "Er ging iets mis: {e!s}"
#: vibes_auth/graphene/mutations.py:305 #: vibes_auth/graphene/mutations.py:305
msgid "passwords do not match" msgid "passwords do not match"
@ -279,7 +279,7 @@ msgstr "Als je dit verzoek niet hebt verzonden, negeer deze e-mail dan."
#: vibes_auth/templates/user_reset_password_email.html:89 #: vibes_auth/templates/user_reset_password_email.html:89
#, python-format #, python-format
msgid "best regards,<br>The %(project_name)s team" msgid "best regards,<br>The %(project_name)s team"
msgstr "Vriendelijke groeten,<br>Het %(project_naam)-team" msgstr "Vriendelijke groeten,<br>Het %(project_name)s-team"
#: vibes_auth/templates/user_reset_password_email.html:95 #: vibes_auth/templates/user_reset_password_email.html:95
#: vibes_auth/templates/user_verification_email.html:107 #: vibes_auth/templates/user_verification_email.html:107
@ -297,7 +297,7 @@ msgid ""
"thank you for signing up for %(project_name)s. please activate your account " "thank you for signing up for %(project_name)s. please activate your account "
"by clicking the button below:" "by clicking the button below:"
msgstr "" msgstr ""
"Bedankt voor het aanmelden bij %(project_naam)s. Activeer je account door op " "Bedankt voor het aanmelden bij %(project_name)s. Activeer je account door op "
"de onderstaande knop te klikken:" "de onderstaande knop te klikken:"
#: vibes_auth/templates/user_verification_email.html:95 #: vibes_auth/templates/user_verification_email.html:95
@ -319,7 +319,7 @@ msgstr ""
#: vibes_auth/templates/user_verification_email.html:101 #: vibes_auth/templates/user_verification_email.html:101
#, python-format #, python-format
msgid "best regards,<br>the %(project_name)s team" msgid "best regards,<br>the %(project_name)s team"
msgstr "Vriendelijke groeten,<br>het %(project_naam)-team" msgstr "Vriendelijke groeten,<br>het %(project_name)s-team"
#: vibes_auth/utils/emailing.py:25 #: vibes_auth/utils/emailing.py:25
#, python-brace-format #, python-brace-format

View file

@ -248,7 +248,7 @@ msgstr "Confirmação de redefinição de senha"
#: vibes_auth/templates/user_verification_email.html:91 #: vibes_auth/templates/user_verification_email.html:91
#, python-format #, python-format
msgid "hello %(user_first_name)s," msgid "hello %(user_first_name)s,"
msgstr "Olá %(nome_primeiro_do_usuário)s," msgstr "Olá %(user_first_name)s,"
#: vibes_auth/templates/user_reset_password_email.html:82 #: vibes_auth/templates/user_reset_password_email.html:82
msgid "" msgid ""
@ -277,7 +277,7 @@ msgstr "Se você não enviou essa solicitação, ignore este e-mail."
#: vibes_auth/templates/user_reset_password_email.html:89 #: vibes_auth/templates/user_reset_password_email.html:89
#, python-format #, python-format
msgid "best regards,<br>The %(project_name)s team" msgid "best regards,<br>The %(project_name)s team"
msgstr "Atenciosamente,<br>A equipe de %(nome_do_projeto)s" msgstr "Atenciosamente,<br>A equipe de %(project_name)s"
#: vibes_auth/templates/user_reset_password_email.html:95 #: vibes_auth/templates/user_reset_password_email.html:95
#: vibes_auth/templates/user_verification_email.html:107 #: vibes_auth/templates/user_verification_email.html:107
@ -295,7 +295,7 @@ msgid ""
"thank you for signing up for %(project_name)s. please activate your account " "thank you for signing up for %(project_name)s. please activate your account "
"by clicking the button below:" "by clicking the button below:"
msgstr "" msgstr ""
"Obrigado por se inscrever no %(nome_do_projeto)s. Ative sua conta clicando " "Obrigado por se inscrever no %(project_name)s. Ative sua conta clicando "
"no botão abaixo:" "no botão abaixo:"
#: vibes_auth/templates/user_verification_email.html:95 #: vibes_auth/templates/user_verification_email.html:95
@ -317,7 +317,7 @@ msgstr ""
#: vibes_auth/templates/user_verification_email.html:101 #: vibes_auth/templates/user_verification_email.html:101
#, python-format #, python-format
msgid "best regards,<br>the %(project_name)s team" msgid "best regards,<br>the %(project_name)s team"
msgstr "Atenciosamente,<br>a equipe de %(nome_do_projeto)s" msgstr "Atenciosamente,<br>a equipe de %(project_name)s"
#: vibes_auth/utils/emailing.py:25 #: vibes_auth/utils/emailing.py:25
#, python-brace-format #, python-brace-format

View file

@ -250,7 +250,7 @@ msgstr "Подтверждение сброса пароля"
#: vibes_auth/templates/user_verification_email.html:91 #: vibes_auth/templates/user_verification_email.html:91
#, python-format #, python-format
msgid "hello %(user_first_name)s," msgid "hello %(user_first_name)s,"
msgstr "Здравствуйте, %(имя_пользователя_первое_имя)s," msgstr "Здравствуйте, %(user_first_name)s,"
#: vibes_auth/templates/user_reset_password_email.html:82 #: vibes_auth/templates/user_reset_password_email.html:82
msgid "" msgid ""
@ -280,7 +280,7 @@ msgstr ""
#: vibes_auth/templates/user_reset_password_email.html:89 #: vibes_auth/templates/user_reset_password_email.html:89
#, python-format #, python-format
msgid "best regards,<br>The %(project_name)s team" msgid "best regards,<br>The %(project_name)s team"
msgstr "С наилучшими пожеланиями,<br>Команда %(название_проекта)" msgstr "С наилучшими пожеланиями,<br>Команда %(project_name)s"
#: vibes_auth/templates/user_reset_password_email.html:95 #: vibes_auth/templates/user_reset_password_email.html:95
#: vibes_auth/templates/user_verification_email.html:107 #: vibes_auth/templates/user_verification_email.html:107
@ -298,7 +298,7 @@ msgid ""
"thank you for signing up for %(project_name)s. please activate your account " "thank you for signing up for %(project_name)s. please activate your account "
"by clicking the button below:" "by clicking the button below:"
msgstr "" msgstr ""
"Спасибо, что зарегистрировались на сайте %(название_проекта)s. Пожалуйста, " "Спасибо, что зарегистрировались на сайте %(project_name)s. Пожалуйста, "
"активируйте свой аккаунт, нажав на кнопку ниже:" "активируйте свой аккаунт, нажав на кнопку ниже:"
#: vibes_auth/templates/user_verification_email.html:95 #: vibes_auth/templates/user_verification_email.html:95
@ -321,7 +321,7 @@ msgstr ""
#: vibes_auth/templates/user_verification_email.html:101 #: vibes_auth/templates/user_verification_email.html:101
#, python-format #, python-format
msgid "best regards,<br>the %(project_name)s team" msgid "best regards,<br>the %(project_name)s team"
msgstr "С наилучшими пожеланиями, <br>команда %(название_проекта)" msgstr "С наилучшими пожеланиями, <br>команда %(project_name)s"
#: vibes_auth/utils/emailing.py:25 #: vibes_auth/utils/emailing.py:25
#, python-brace-format #, python-brace-format

View file

@ -72,7 +72,7 @@ msgstr "畸形电话号码: {phone_number}"
#: vibes_auth/graphene/mutations.py:134 #: vibes_auth/graphene/mutations.py:134
#, python-brace-format #, python-brace-format
msgid "Invalid attribute format: {attribute_pair}" msgid "Invalid attribute format: {attribute_pair}"
msgstr "属性格式无效:{属性对}" msgstr "属性格式无效:{attribute_pair}"
#: vibes_auth/graphene/mutations.py:261 #: vibes_auth/graphene/mutations.py:261
msgid "activation link is invalid!" msgid "activation link is invalid!"
@ -84,7 +84,7 @@ msgstr "帐户已激活..."
#: vibes_auth/graphene/mutations.py:271 vibes_auth/graphene/mutations.py:321 #: vibes_auth/graphene/mutations.py:271 vibes_auth/graphene/mutations.py:321
msgid "something went wrong: {e!s}" msgid "something went wrong: {e!s}"
msgstr "出了问题: {e!" msgstr "出了问题:{e!s}"
#: vibes_auth/graphene/mutations.py:305 #: vibes_auth/graphene/mutations.py:305
msgid "passwords do not match" msgid "passwords do not match"
@ -248,7 +248,7 @@ msgstr "密码重置确认"
#: vibes_auth/templates/user_verification_email.html:91 #: vibes_auth/templates/user_verification_email.html:91
#, python-format #, python-format
msgid "hello %(user_first_name)s," msgid "hello %(user_first_name)s,"
msgstr "您好 %(用户名)s、" msgstr "您好 %(user_first_name)s、"
#: vibes_auth/templates/user_reset_password_email.html:82 #: vibes_auth/templates/user_reset_password_email.html:82
msgid "" msgid ""
@ -273,7 +273,7 @@ msgstr "如果您没有发送此请求,请忽略此邮件。"
#: vibes_auth/templates/user_reset_password_email.html:89 #: vibes_auth/templates/user_reset_password_email.html:89
#, python-format #, python-format
msgid "best regards,<br>The %(project_name)s team" msgid "best regards,<br>The %(project_name)s team"
msgstr "致以最诚挚的问候,<br>%(项目名称)团队" msgstr "致以最诚挚的问候,<br>%(project_name)s团队"
#: vibes_auth/templates/user_reset_password_email.html:95 #: vibes_auth/templates/user_reset_password_email.html:95
#: vibes_auth/templates/user_verification_email.html:107 #: vibes_auth/templates/user_verification_email.html:107
@ -290,7 +290,7 @@ msgstr "激活账户"
msgid "" msgid ""
"thank you for signing up for %(project_name)s. please activate your account " "thank you for signing up for %(project_name)s. please activate your account "
"by clicking the button below:" "by clicking the button below:"
msgstr "感谢您注册 %(项目名称)s。请点击下面的按钮激活您的账户" msgstr "感谢您注册 %(project_name)s。请点击下面的按钮激活您的账户"
#: vibes_auth/templates/user_verification_email.html:95 #: vibes_auth/templates/user_verification_email.html:95
msgid "" msgid ""
@ -311,7 +311,7 @@ msgstr ""
#: vibes_auth/templates/user_verification_email.html:101 #: vibes_auth/templates/user_verification_email.html:101
#, python-format #, python-format
msgid "best regards,<br>the %(project_name)s team" msgid "best regards,<br>the %(project_name)s team"
msgstr "致以最诚挚的问候,<br>%(项目名称)团队" msgstr "致以最诚挚的问候,<br>%(project_name)s团队"
#: vibes_auth/utils/emailing.py:25 #: vibes_auth/utils/emailing.py:25
#, python-brace-format #, python-brace-format