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:
parent
85a49c4e8b
commit
76d490f2e2
69 changed files with 5611 additions and 4967 deletions
|
|
@ -263,12 +263,17 @@ class OrderProductInline(admin.TabularInline):
|
|||
|
||||
@admin.register(Order)
|
||||
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")
|
||||
search_fields = ("user__email", "status")
|
||||
search_fields = ("user__email", "status", "uuid", "human_readable_id")
|
||||
inlines = [OrderProductInline]
|
||||
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):
|
||||
qs = super().get_queryset(request)
|
||||
|
|
|
|||
62
core/api_urls.py
Normal file
62
core/api_urls.py
Normal 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
11
core/b2b_urls.py
Normal 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"),
|
||||
]
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
{
|
||||
"payment_methods": [
|
||||
"CASH",
|
||||
"CARD"
|
||||
"CARD",
|
||||
"WIRE"
|
||||
]
|
||||
}
|
||||
|
|
@ -8,7 +8,9 @@ from core.serializers import (
|
|||
CacheOperatorSerializer,
|
||||
ContactUsSerializer,
|
||||
LanguageSerializer,
|
||||
BuyAsBusinessOrderSerializer,
|
||||
)
|
||||
from payments.serializers import TransactionProcessSerializer
|
||||
|
||||
CACHE_SCHEMA = {
|
||||
"post": extend_schema(
|
||||
|
|
@ -82,3 +84,17 @@ SEARCH_SCHEMA = {
|
|||
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`.")
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
@ -196,7 +196,7 @@ class BuyOrder(BaseMutation):
|
|||
|
||||
class BuyUnregisteredOrder(BaseMutation):
|
||||
class Meta:
|
||||
description = _("buy an unregistered order")
|
||||
description = _("purchase an order without account creation")
|
||||
|
||||
class Arguments:
|
||||
products = List(UUID, required=True)
|
||||
|
|
|
|||
|
|
@ -280,6 +280,7 @@ class OrderType(DjangoObjectType):
|
|||
"total_price",
|
||||
"total_quantity",
|
||||
"is_whole_digital",
|
||||
"human_readable_id",
|
||||
)
|
||||
description = _("orders")
|
||||
|
||||
|
|
|
|||
Binary file not shown.
File diff suppressed because it is too large
Load diff
Binary file not shown.
File diff suppressed because it is too large
Load diff
Binary file not shown.
File diff suppressed because it is too large
Load diff
Binary file not shown.
File diff suppressed because it is too large
Load diff
Binary file not shown.
File diff suppressed because it is too large
Load diff
Binary file not shown.
File diff suppressed because it is too large
Load diff
Binary file not shown.
File diff suppressed because it is too large
Load diff
Binary file not shown.
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
Binary file not shown.
File diff suppressed because it is too large
Load diff
Binary file not shown.
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
Binary file not shown.
File diff suppressed because it is too large
Load diff
Binary file not shown.
File diff suppressed because it is too large
Load diff
Binary file not shown.
File diff suppressed because it is too large
Load diff
Binary file not shown.
File diff suppressed because it is too large
Load diff
Binary file not shown.
File diff suppressed because it is too large
Load diff
Binary file not shown.
File diff suppressed because it is too large
Load diff
|
|
@ -1,12 +1,13 @@
|
|||
import os
|
||||
import re
|
||||
import tempfile
|
||||
from tempfile import NamedTemporaryFile
|
||||
|
||||
import polib
|
||||
import requests
|
||||
from django.apps import apps
|
||||
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",
|
||||
|
|
@ -28,49 +29,72 @@ DEEPL_TARGET_LANGUAGES_MAPPING = {
|
|||
"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
|
||||
(inserting the missing space) and strip blank-header entries, then parse again.
|
||||
Replace placeholders with tokens and collect originals.
|
||||
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:
|
||||
return polib.pofile(path)
|
||||
except OSError:
|
||||
text = open(path, encoding="utf-8").read()
|
||||
# ensure any "#,fuzzy" becomes "#, fuzzy" so polib can parse flags
|
||||
text = re.sub(r'^#,(?!\s)', '#, ', text, flags=re.MULTILINE)
|
||||
|
||||
# split off the header entry by the first blank line
|
||||
with open(path, encoding="utf-8") as f:
|
||||
text = f.read()
|
||||
# fix malformed fuzzy flags
|
||||
text = re.sub(r"^#,(?!\s)", "#, ", text, flags=re.MULTILINE)
|
||||
# remove empty header entries
|
||||
parts = text.split("\n\n", 1)
|
||||
header = parts[0]
|
||||
rest = parts[1] if len(parts) > 1 else ""
|
||||
|
||||
# drop any stray blank msgid/msgstr pairs that can also break parsing
|
||||
rest_clean = re.sub(
|
||||
r'^msgid ""\s*\nmsgstr ""\s*\n?', "",
|
||||
r"^msgid \"\"\s*\nmsgstr \"\"\s*\n?", "",
|
||||
rest,
|
||||
flags=re.MULTILINE
|
||||
)
|
||||
|
||||
sanitized = header + "\n\n" + rest_clean
|
||||
|
||||
# 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()
|
||||
tmp = NamedTemporaryFile(mode="w+", delete=False, suffix=".po", encoding="utf-8") # noqa: SIM115
|
||||
try:
|
||||
po = polib.pofile(tmp.name)
|
||||
tmp.write(sanitized)
|
||||
tmp.flush()
|
||||
tmp.close()
|
||||
return polib.pofile(tmp.name)
|
||||
finally:
|
||||
os.unlink(tmp.name)
|
||||
return po
|
||||
try:
|
||||
os.unlink(tmp.name)
|
||||
except OSError as e:
|
||||
raise CommandError("Failed to write sanitized .po file") from e
|
||||
|
||||
|
||||
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):
|
||||
parser.add_argument(
|
||||
|
|
@ -79,7 +103,7 @@ class Command(BaseCommand):
|
|||
action="append",
|
||||
required=True,
|
||||
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(
|
||||
"-a", "--app",
|
||||
|
|
@ -87,138 +111,110 @@ class Command(BaseCommand):
|
|||
action="append",
|
||||
required=True,
|
||||
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):
|
||||
target_langs = options["target_languages"]
|
||||
target_apps = set(options["target_apps"])
|
||||
auth_key = os.environ.get("DEEPL_AUTH_KEY")
|
||||
def handle(self, *args, **options) -> None:
|
||||
target_langs: list[str] = options['target_languages']
|
||||
target_apps: set[str] = set(options['target_apps'])
|
||||
auth_key = os.environ.get('DEEPL_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:
|
||||
api_code = DEEPL_TARGET_LANGUAGES_MAPPING.get(target_lang)
|
||||
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
|
||||
if api_code == "unsupported":
|
||||
self.stdout.write(self.style.WARNING(f"Skipping unsupported language '{target_lang}'"))
|
||||
if api_code == 'unsupported':
|
||||
self.stdout.write(self.style.WARNING(f"Unsupported language '{target_lang}'"))
|
||||
continue
|
||||
|
||||
self.stdout.write(self.style.MIGRATE_HEADING(
|
||||
f"→ Translating into {target_lang} (DeepL code: {api_code})"
|
||||
))
|
||||
self.stdout.write(self.style.MIGRATE_HEADING(f"→ Translating into {target_lang}"))
|
||||
|
||||
for app_config in apps.get_app_configs():
|
||||
if app_config.label not in target_apps:
|
||||
for app_conf in apps.get_app_configs():
|
||||
if app_conf.label not in target_apps:
|
||||
continue
|
||||
|
||||
# 1) load & sanitize English source .po
|
||||
en_po_path = os.path.join(
|
||||
app_config.path, "locale", "en_GB", "LC_MESSAGES", "django.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"
|
||||
))
|
||||
en_path = os.path.join(app_conf.path, 'locale', 'en_GB', 'LC_MESSAGES', 'django.po')
|
||||
if not os.path.isfile(en_path):
|
||||
self.stdout.write(self.style.WARNING(f"• {app_conf.label}: no en_GB PO"))
|
||||
continue
|
||||
|
||||
self.stdout.write(f" • {app_config.label}: loading English PO…")
|
||||
en_po = load_po_sanitized(en_po_path)
|
||||
self.stdout.write(f"• {app_conf.label}: loading English PO…")
|
||||
en_po = load_po_sanitized(en_path)
|
||||
|
||||
# collect all non-obsolete entries with a msgid
|
||||
en_entries = [
|
||||
e for e in en_po
|
||||
if e.msgid and not e.obsolete
|
||||
]
|
||||
# map msgid -> source text (prefer existing msgstr if any)
|
||||
source_texts = {
|
||||
e.msgid: (e.msgstr or e.msgid)
|
||||
for e in en_entries
|
||||
}
|
||||
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"))
|
||||
for e in missing:
|
||||
e.msgstr = input(f"Enter translation for '{e.msgid}': ").strip()
|
||||
en_po.save(en_path)
|
||||
self.stdout.write(self.style.SUCCESS("Updated en_GB PO"))
|
||||
|
||||
# 2) load (or create) the target .po
|
||||
tgt_po_dir = os.path.join(
|
||||
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")
|
||||
entries = [e for e in en_po if e.msgid and not e.obsolete]
|
||||
source_map = {e.msgid: e.msgstr for e in entries}
|
||||
|
||||
if os.path.exists(tgt_po_path):
|
||||
self.stdout.write(f" loading existing {target_lang} PO…")
|
||||
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(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(tgt_po_path)
|
||||
except Exception:
|
||||
self.stdout.write(self.style.WARNING(
|
||||
" ! existing target PO parse error, starting fresh"
|
||||
))
|
||||
old_tgt = None
|
||||
else:
|
||||
old_tgt = None
|
||||
old_tgt = load_po_sanitized(tgt_path)
|
||||
except Exception as e:
|
||||
self.stdout.write(self.style.WARNING(f"Existing PO parse error({e!s}), starting fresh"))
|
||||
|
||||
# rebuild a new PO file in the same order as English
|
||||
new_po = polib.POFile()
|
||||
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
|
||||
entry = polib.POEntry(
|
||||
new_po.append(polib.POEntry(
|
||||
msgid=e.msgid,
|
||||
msgstr=(prev.msgstr if prev and prev.msgstr else ""),
|
||||
msgstr=prev.msgstr if prev and prev.msgstr else '',
|
||||
msgctxt=e.msgctxt,
|
||||
comment=e.comment,
|
||||
tcomment=e.tcomment,
|
||||
occurrences=e.occurrences,
|
||||
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
|
||||
|
||||
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
|
||||
resp = requests.post(
|
||||
"https://api-free.deepl.com/v2/translate",
|
||||
data={
|
||||
"auth_key": auth_key,
|
||||
"target_lang": api_code,
|
||||
"text": texts,
|
||||
}
|
||||
)
|
||||
data = [
|
||||
('auth_key', auth_key),
|
||||
('target_lang', api_code),
|
||||
] + [('text', t) for t in protected]
|
||||
resp = requests.post('https://api-free.deepl.com/v2/translate', data=data)
|
||||
try:
|
||||
resp.raise_for_status()
|
||||
data = resp.json()
|
||||
result = resp.json()
|
||||
except Exception as exc:
|
||||
raise CommandError(
|
||||
f"DeepL API error for {app_config.label}|{target_lang}: "
|
||||
f"{exc} – {resp.text}"
|
||||
)
|
||||
raise CommandError(f"DeepL error: {exc} – {resp.text}")
|
||||
|
||||
translations = data.get("translations", [])
|
||||
if len(translations) != len(to_translate):
|
||||
raise CommandError(
|
||||
f"Unexpected translations count: {len(translations)} vs {len(to_translate)}"
|
||||
)
|
||||
trans = result.get('translations', [])
|
||||
if len(trans) != len(to_trans):
|
||||
raise CommandError(f"Got {len(trans)} translations, expected {len(to_trans)}")
|
||||
|
||||
for entry, trans in zip(to_translate, translations):
|
||||
entry.msgstr = trans["text"]
|
||||
for e, obj, pmap in zip(to_trans, trans, maps):
|
||||
e.msgstr = deplaceholderize(obj['text'], pmap)
|
||||
|
||||
# 5) save merged & translated PO
|
||||
new_po.save(tgt_po_path)
|
||||
self.stdout.write(self.style.SUCCESS(
|
||||
f" ✔ saved {target_lang} PO: {tgt_po_path}"
|
||||
))
|
||||
new_po.save(tgt_path)
|
||||
self.stdout.write(self.style.SUCCESS(f"Saved {tgt_path}"))
|
||||
|
||||
self.stdout.write(self.style.SUCCESS("All translations complete."))
|
||||
self.stdout.write(self.style.SUCCESS("Done."))
|
||||
|
|
|
|||
150
core/management/commands/purify_translated.py
Normal file
150
core/management/commands/purify_translated.py
Normal 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."))
|
||||
|
|
@ -40,7 +40,7 @@ from mptt.models import MPTTModel
|
|||
from core.abstract import NiceModel
|
||||
from core.choices import ORDER_PRODUCT_STATUS_CHOICES, ORDER_STATUS_CHOICES
|
||||
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.validators import validate_category_image_dimensions
|
||||
from evibes.settings import CURRENCY_CODE
|
||||
|
|
@ -493,6 +493,13 @@ class Order(NiceModel):
|
|||
null=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:
|
||||
verbose_name = _("order")
|
||||
|
|
@ -501,6 +508,10 @@ class Order(NiceModel):
|
|||
def __str__(self) -> str:
|
||||
return f"{self.pk} Order for {self.user.email}"
|
||||
|
||||
@property
|
||||
def is_business(self) -> bool:
|
||||
return self.attributes.get("is_business", False)
|
||||
|
||||
@property
|
||||
def total_price(self) -> float:
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -81,3 +81,13 @@ class BuyUnregisteredOrderSerializer(Serializer):
|
|||
billing_customer_address = UnregisteredCustomerAddressSerializer(required=True)
|
||||
shipping_customer_address = UnregisteredCustomerAddressSerializer(required=False)
|
||||
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)
|
||||
|
|
|
|||
|
|
@ -411,6 +411,7 @@ class OrderDetailSerializer(ModelSerializer):
|
|||
"shipping_address",
|
||||
"buy_time",
|
||||
"order_products",
|
||||
"human_readable_id",
|
||||
"created",
|
||||
"modified",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -291,6 +291,7 @@ class OrderSimpleSerializer(ModelSerializer):
|
|||
model = Order
|
||||
fields = [
|
||||
"uuid",
|
||||
"human_readable_id",
|
||||
"status",
|
||||
"user",
|
||||
"promo_code",
|
||||
|
|
|
|||
63
core/urls.py
63
core/urls.py
|
|
@ -1,62 +1 @@
|
|||
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"),
|
||||
]
|
||||
urlpatterns = []
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import logging
|
||||
import re
|
||||
import secrets
|
||||
from contextlib import contextmanager
|
||||
|
||||
from constance import config
|
||||
|
|
@ -126,3 +127,21 @@ def resolve_translations_for_elasticsearch(instance, field_name):
|
|||
filled_field = getattr(instance, field_name, "")
|
||||
if not 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)
|
||||
|
|
|
|||
|
|
@ -33,13 +33,15 @@ from core.docs.drf.views import (
|
|||
PARAMETERS_SCHEMA,
|
||||
REQUEST_CURSED_URL_SCHEMA,
|
||||
SEARCH_SCHEMA,
|
||||
BUY_AS_BUSINESS_SCHEMA,
|
||||
)
|
||||
from core.elasticsearch import process_query
|
||||
from core.models import DigitalAssetDownload
|
||||
from core.models import DigitalAssetDownload, Order
|
||||
from core.serializers import (
|
||||
CacheOperatorSerializer,
|
||||
ContactUsSerializer,
|
||||
LanguageSerializer,
|
||||
BuyAsBusinessOrderSerializer,
|
||||
)
|
||||
from core.utils import get_project_parameters, is_url_safe
|
||||
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 evibes import settings
|
||||
from evibes.settings import LANGUAGES
|
||||
from payments.serializers import TransactionProcessSerializer
|
||||
|
||||
|
||||
def sitemap_index(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
|
||||
|
||||
|
||||
def sitemap_detail(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
|
||||
|
||||
|
||||
|
|
@ -190,12 +193,34 @@ class GlobalSearchView(APIView):
|
|||
A global search endpoint.
|
||||
It returns a response grouping matched items by index.
|
||||
"""
|
||||
|
||||
renderer_classes = [CamelCaseJSONRenderer, MultiPartRenderer, XMLRenderer, YAMLRenderer]
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
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):
|
||||
try:
|
||||
uuid = force_str(urlsafe_base64_decode(kwargs["encoded_uuid"]))
|
||||
|
|
|
|||
|
|
@ -73,6 +73,7 @@ from core.serializers import (
|
|||
VendorSimpleSerializer,
|
||||
WishlistDetailSerializer,
|
||||
WishlistSimpleSerializer,
|
||||
BuyAsBusinessOrderSerializer,
|
||||
)
|
||||
from core.utils import format_attributes
|
||||
from core.utils.messages import permission_denied_message
|
||||
|
|
@ -228,16 +229,16 @@ class OrderViewSet(EvibesViewSet):
|
|||
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"))
|
||||
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)
|
||||
|
||||
@action(detail=True, methods=["post"], url_path="add_order_product")
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ urlpatterns = [
|
|||
path(r"i18n/", include("django.conf.urls.i18n")),
|
||||
path(r"favicon.ico", favicon_view),
|
||||
path(r"", index),
|
||||
path(r"", include("core.urls")),
|
||||
path(r"", include("core.api_urls")),
|
||||
path(r"auth/", include("vibes_auth.urls")),
|
||||
path(r"geo/", include("geo.urls")),
|
||||
path(r"payments/", include("payments.urls")),
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
# This file is distributed under the same license as the eVibes package.
|
||||
# EGOR GORBUNOV <CONTACT@FUREUNOIR.COM>, 2025.
|
||||
#
|
||||
#,fuzzy
|
||||
#, fuzzy
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: 1\n"
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
# This file is distributed under the same license as the eVibes package.
|
||||
# EGOR GORBUNOV <CONTACT@FUREUNOIR.COM>, 2025.
|
||||
#
|
||||
#,fuzzy
|
||||
#, fuzzy
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: 1\n"
|
||||
|
|
|
|||
|
|
@ -14,5 +14,8 @@ def create_balance_on_user_creation_signal(instance, created, **kwargs):
|
|||
@receiver(post_save, sender=Transaction)
|
||||
def process_transaction_changes(instance, created, **kwargs):
|
||||
if created:
|
||||
gateway = object()
|
||||
gateway.process_transaction(instance)
|
||||
try:
|
||||
gateway = object()
|
||||
gateway.process_transaction(instance)
|
||||
except Exception: # noqa:
|
||||
instance.process = {"status": "NOGATEWAY"}
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -60,7 +60,7 @@ msgstr ""
|
|||
#: vibes_auth/graphene/mutations.py:103
|
||||
#, python-brace-format
|
||||
msgid "{name} does not exist: {uuid}"
|
||||
msgstr "{Name} existiert nicht: {uuid}"
|
||||
msgstr "{name} existiert nicht: {uuid}"
|
||||
|
||||
#: vibes_auth/graphene/mutations.py:111
|
||||
msgid "malformed email"
|
||||
|
|
@ -74,7 +74,7 @@ msgstr "Fehlerhafte Telefonnummer: {phone_number}"
|
|||
#: vibes_auth/graphene/mutations.py:134
|
||||
#, python-brace-format
|
||||
msgid "Invalid attribute format: {attribute_pair}"
|
||||
msgstr "Ungültiges Attributformat: {Attribut_paar}"
|
||||
msgstr "Ungültiges Attributformat: {attribute_pair}"
|
||||
|
||||
#: vibes_auth/graphene/mutations.py:261
|
||||
msgid "activation link is invalid!"
|
||||
|
|
@ -282,7 +282,7 @@ msgstr ""
|
|||
#: vibes_auth/templates/user_reset_password_email.html:89
|
||||
#, python-format
|
||||
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_verification_email.html:107
|
||||
|
|
@ -300,7 +300,7 @@ msgid ""
|
|||
"thank you for signing up for %(project_name)s. please activate your account "
|
||||
"by clicking the button below:"
|
||||
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:"
|
||||
|
||||
#: vibes_auth/templates/user_verification_email.html:95
|
||||
|
|
@ -323,7 +323,7 @@ msgstr ""
|
|||
#: vibes_auth/templates/user_verification_email.html:101
|
||||
#, python-format
|
||||
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
|
||||
#, python-brace-format
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -59,7 +59,7 @@ msgstr ""
|
|||
#: vibes_auth/graphene/mutations.py:103
|
||||
#, python-brace-format
|
||||
msgid "{name} does not exist: {uuid}"
|
||||
msgstr "{nombre} no existe: {uuid}"
|
||||
msgstr "{name} no existe: {uuid}"
|
||||
|
||||
#: vibes_auth/graphene/mutations.py:111
|
||||
msgid "malformed email"
|
||||
|
|
@ -73,7 +73,7 @@ msgstr "Número de teléfono malformado: {phone_number}"
|
|||
#: vibes_auth/graphene/mutations.py:134
|
||||
#, python-brace-format
|
||||
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
|
||||
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
|
||||
msgid "something went wrong: {e!s}"
|
||||
msgstr "Algo salió mal."
|
||||
msgstr "Algo salió mal: {e!s}."
|
||||
|
||||
#: vibes_auth/graphene/mutations.py:305
|
||||
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
|
||||
#, python-format
|
||||
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
|
||||
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
|
||||
#, python-format
|
||||
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_verification_email.html:107
|
||||
|
|
@ -296,7 +296,7 @@ msgid ""
|
|||
"thank you for signing up for %(project_name)s. please activate your account "
|
||||
"by clicking the button below:"
|
||||
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:"
|
||||
|
||||
#: vibes_auth/templates/user_verification_email.html:95
|
||||
|
|
@ -318,7 +318,7 @@ msgstr ""
|
|||
#: vibes_auth/templates/user_verification_email.html:101
|
||||
#, python-format
|
||||
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
|
||||
#, 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
|
||||
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
|
||||
#, python-brace-format
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -72,7 +72,7 @@ msgstr "不正な電話番号:{phone_number}。"
|
|||
#: vibes_auth/graphene/mutations.py:134
|
||||
#, python-brace-format
|
||||
msgid "Invalid attribute format: {attribute_pair}"
|
||||
msgstr "無効な属性形式です:{属性ペア}。"
|
||||
msgstr "無効な属性形式です:{attribute_pair}。"
|
||||
|
||||
#: vibes_auth/graphene/mutations.py:261
|
||||
msgid "activation link is invalid!"
|
||||
|
|
@ -84,7 +84,7 @@ msgstr "アカウントはすでに有効になっています..."
|
|||
|
||||
#: vibes_auth/graphene/mutations.py:271 vibes_auth/graphene/mutations.py:321
|
||||
msgid "something went wrong: {e!s}"
|
||||
msgstr "何かが間違っていた:{e!}"
|
||||
msgstr "何かが間違っていた:{e!s}"
|
||||
|
||||
#: vibes_auth/graphene/mutations.py:305
|
||||
msgid "passwords do not match"
|
||||
|
|
@ -277,7 +277,7 @@ msgstr "このリクエストを送信していない場合は、このメール
|
|||
#: vibes_auth/templates/user_reset_password_email.html:89
|
||||
#, python-format
|
||||
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_verification_email.html:107
|
||||
|
|
@ -295,7 +295,7 @@ msgid ""
|
|||
"thank you for signing up for %(project_name)s. please activate your account "
|
||||
"by clicking the button below:"
|
||||
msgstr ""
|
||||
"(project_name)sにご登録いただきありがとうございます。下のボタンをクリックして"
|
||||
"%(project_name)sにご登録いただきありがとうございます。下のボタンをクリックして"
|
||||
"アカウントを有効にしてください:"
|
||||
|
||||
#: vibes_auth/templates/user_verification_email.html:95
|
||||
|
|
@ -318,12 +318,12 @@ msgstr ""
|
|||
#: vibes_auth/templates/user_verification_email.html:101
|
||||
#, python-format
|
||||
msgid "best regards,<br>the %(project_name)s team"
|
||||
msgstr "よろしくお願いします、<br>%(プロジェクト名)のチーム。"
|
||||
msgstr "よろしくお願いします、<br>%(project_name)sのチーム。"
|
||||
|
||||
#: vibes_auth/utils/emailing.py:25
|
||||
#, python-brace-format
|
||||
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
|
||||
#, python-brace-format
|
||||
|
|
@ -332,12 +332,12 @@ msgstr "指定されたUUIDを持つユーザーが見つかりません:{user
|
|||
|
||||
#: vibes_auth/utils/emailing.py:49 vibes_auth/utils/emailing.py:90
|
||||
msgid "something went wrong while sending an email: {e!s}"
|
||||
msgstr "メール送信中に何か問題が発生しました:{e!}"
|
||||
msgstr "メール送信中に何か問題が発生しました:{e!s}"
|
||||
|
||||
#: vibes_auth/utils/emailing.py:65
|
||||
#, python-brace-format
|
||||
msgid "{config.PROJECT_NAME} | Reset Password"
|
||||
msgstr "{コンフィグ.PROJECT_NAME}。| パスワードのリセット"
|
||||
msgstr "{config.PROJECT_NAME}。| パスワードのリセット"
|
||||
|
||||
#: vibes_auth/validators.py:13
|
||||
msgid ""
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -60,7 +60,7 @@ msgstr ""
|
|||
#: vibes_auth/graphene/mutations.py:103
|
||||
#, python-brace-format
|
||||
msgid "{name} does not exist: {uuid}"
|
||||
msgstr "{naam} bestaat niet: {uuid}"
|
||||
msgstr "{name} bestaat niet: {uuid}"
|
||||
|
||||
#: vibes_auth/graphene/mutations.py:111
|
||||
msgid "malformed email"
|
||||
|
|
@ -74,7 +74,7 @@ msgstr "Misvormd telefoonnummer: {phone_number}"
|
|||
#: vibes_auth/graphene/mutations.py:134
|
||||
#, python-brace-format
|
||||
msgid "Invalid attribute format: {attribute_pair}"
|
||||
msgstr "Ongeldig attribuutformaat: {attribuut_paar}"
|
||||
msgstr "Ongeldig attribuutformaat: {attribute_pair}"
|
||||
|
||||
#: vibes_auth/graphene/mutations.py:261
|
||||
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
|
||||
msgid "something went wrong: {e!s}"
|
||||
msgstr "Er ging iets mis."
|
||||
msgstr "Er ging iets mis: {e!s}"
|
||||
|
||||
#: vibes_auth/graphene/mutations.py:305
|
||||
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
|
||||
#, python-format
|
||||
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_verification_email.html:107
|
||||
|
|
@ -297,7 +297,7 @@ msgid ""
|
|||
"thank you for signing up for %(project_name)s. please activate your account "
|
||||
"by clicking the button below:"
|
||||
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:"
|
||||
|
||||
#: vibes_auth/templates/user_verification_email.html:95
|
||||
|
|
@ -319,7 +319,7 @@ msgstr ""
|
|||
#: vibes_auth/templates/user_verification_email.html:101
|
||||
#, python-format
|
||||
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
|
||||
#, python-brace-format
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -248,7 +248,7 @@ msgstr "Confirmação de redefinição de senha"
|
|||
#: vibes_auth/templates/user_verification_email.html:91
|
||||
#, python-format
|
||||
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
|
||||
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
|
||||
#, python-format
|
||||
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_verification_email.html:107
|
||||
|
|
@ -295,7 +295,7 @@ msgid ""
|
|||
"thank you for signing up for %(project_name)s. please activate your account "
|
||||
"by clicking the button below:"
|
||||
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:"
|
||||
|
||||
#: vibes_auth/templates/user_verification_email.html:95
|
||||
|
|
@ -317,7 +317,7 @@ msgstr ""
|
|||
#: vibes_auth/templates/user_verification_email.html:101
|
||||
#, python-format
|
||||
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
|
||||
#, python-brace-format
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -250,7 +250,7 @@ msgstr "Подтверждение сброса пароля"
|
|||
#: vibes_auth/templates/user_verification_email.html:91
|
||||
#, python-format
|
||||
msgid "hello %(user_first_name)s,"
|
||||
msgstr "Здравствуйте, %(имя_пользователя_первое_имя)s,"
|
||||
msgstr "Здравствуйте, %(user_first_name)s,"
|
||||
|
||||
#: vibes_auth/templates/user_reset_password_email.html:82
|
||||
msgid ""
|
||||
|
|
@ -280,7 +280,7 @@ msgstr ""
|
|||
#: vibes_auth/templates/user_reset_password_email.html:89
|
||||
#, python-format
|
||||
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_verification_email.html:107
|
||||
|
|
@ -298,7 +298,7 @@ msgid ""
|
|||
"thank you for signing up for %(project_name)s. please activate your account "
|
||||
"by clicking the button below:"
|
||||
msgstr ""
|
||||
"Спасибо, что зарегистрировались на сайте %(название_проекта)s. Пожалуйста, "
|
||||
"Спасибо, что зарегистрировались на сайте %(project_name)s. Пожалуйста, "
|
||||
"активируйте свой аккаунт, нажав на кнопку ниже:"
|
||||
|
||||
#: vibes_auth/templates/user_verification_email.html:95
|
||||
|
|
@ -321,7 +321,7 @@ msgstr ""
|
|||
#: vibes_auth/templates/user_verification_email.html:101
|
||||
#, python-format
|
||||
msgid "best regards,<br>the %(project_name)s team"
|
||||
msgstr "С наилучшими пожеланиями, <br>команда %(название_проекта)"
|
||||
msgstr "С наилучшими пожеланиями, <br>команда %(project_name)s"
|
||||
|
||||
#: vibes_auth/utils/emailing.py:25
|
||||
#, python-brace-format
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -72,7 +72,7 @@ msgstr "畸形电话号码: {phone_number}"
|
|||
#: vibes_auth/graphene/mutations.py:134
|
||||
#, python-brace-format
|
||||
msgid "Invalid attribute format: {attribute_pair}"
|
||||
msgstr "属性格式无效:{属性对}"
|
||||
msgstr "属性格式无效:{attribute_pair}"
|
||||
|
||||
#: vibes_auth/graphene/mutations.py:261
|
||||
msgid "activation link is invalid!"
|
||||
|
|
@ -84,7 +84,7 @@ msgstr "帐户已激活..."
|
|||
|
||||
#: vibes_auth/graphene/mutations.py:271 vibes_auth/graphene/mutations.py:321
|
||||
msgid "something went wrong: {e!s}"
|
||||
msgstr "出了问题: {e!}"
|
||||
msgstr "出了问题:{e!s}"
|
||||
|
||||
#: vibes_auth/graphene/mutations.py:305
|
||||
msgid "passwords do not match"
|
||||
|
|
@ -248,7 +248,7 @@ msgstr "密码重置确认"
|
|||
#: vibes_auth/templates/user_verification_email.html:91
|
||||
#, python-format
|
||||
msgid "hello %(user_first_name)s,"
|
||||
msgstr "您好 %(用户名)s、"
|
||||
msgstr "您好 %(user_first_name)s、"
|
||||
|
||||
#: vibes_auth/templates/user_reset_password_email.html:82
|
||||
msgid ""
|
||||
|
|
@ -273,7 +273,7 @@ msgstr "如果您没有发送此请求,请忽略此邮件。"
|
|||
#: vibes_auth/templates/user_reset_password_email.html:89
|
||||
#, python-format
|
||||
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_verification_email.html:107
|
||||
|
|
@ -290,7 +290,7 @@ msgstr "激活账户"
|
|||
msgid ""
|
||||
"thank you for signing up for %(project_name)s. please activate your account "
|
||||
"by clicking the button below:"
|
||||
msgstr "感谢您注册 %(项目名称)s。请点击下面的按钮激活您的账户:"
|
||||
msgstr "感谢您注册 %(project_name)s。请点击下面的按钮激活您的账户:"
|
||||
|
||||
#: vibes_auth/templates/user_verification_email.html:95
|
||||
msgid ""
|
||||
|
|
@ -311,7 +311,7 @@ msgstr ""
|
|||
#: vibes_auth/templates/user_verification_email.html:101
|
||||
#, python-format
|
||||
msgid "best regards,<br>the %(project_name)s team"
|
||||
msgstr "致以最诚挚的问候,<br>%(项目名称)团队"
|
||||
msgstr "致以最诚挚的问候,<br>%(project_name)s团队"
|
||||
|
||||
#: vibes_auth/utils/emailing.py:25
|
||||
#, python-brace-format
|
||||
|
|
|
|||
Loading…
Reference in a new issue