update translation and more

This commit is contained in:
ismail 2025-11-23 13:45:21 +03:00
parent e1a55d7633
commit 852eac84bf
24 changed files with 43870 additions and 662 deletions

188
base.po Normal file
View File

@ -0,0 +1,188 @@
msgid ""
msgstr ""
"Project-Id-Version: Big SaaS App 2.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-05-20 10:00+0000\n"
"PO-Revision-Date: \n"
"Last-Translator: \n"
"Language-Team: \n"
"Language: es\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
msgid "Dashboard"
msgstr ""
msgid "My Profile"
msgstr ""
msgid "Account Settings"
msgstr ""
msgid "Billing & Invoices"
msgstr ""
msgid "Log Out"
msgstr ""
msgid "Email Address"
msgstr ""
msgid "Password"
msgstr ""
msgid "Remember me on this device"
msgstr ""
msgid "Forgot your password?"
msgstr ""
msgid "Don't have an account? Sign up."
msgstr ""
msgid "Ensure this field has at least %(limit_value)d characters (it has %(show_value)d)."
msgstr ""
msgctxt "noun"
msgid "Book"
msgstr ""
msgctxt "verb"
msgid "Book"
msgstr ""
msgctxt "month_name"
msgid "May"
msgstr ""
msgctxt "auxiliary_verb"
msgid "May"
msgstr ""
msgid "Product Description"
msgstr ""
msgid "Add to Cart"
msgstr ""
msgid "Proceed to Checkout"
msgstr ""
msgid "Total: $%(amount).2f"
msgstr ""
msgid "Shipping Address"
msgstr ""
msgid "Order History"
msgstr ""
#, fuzzy
msgid "Delete Account"
msgstr "Borrar cuenta permanentemente ahora mismo"
#, fuzzy
msgid "Save Changes"
msgstr "Guardar cosas"
#, fuzzy
msgid "Upload Avatar"
msgstr "Subir foto"
msgid ""
"Welcome to the platform. By using our services, you agree to our <a "
"href='%(terms_url)s'>Terms of Service</a> and <a "
"href='%(privacy_url)s'>Privacy Policy</a>."
msgstr ""
msgid ""
"Please check your email inbox. We have sent a confirmation link to verify "
"your account ownership. The link will expire in 24 hours."
msgstr ""
msgid "<strong>Warning:</strong> This action cannot be undone."
msgstr ""
msgid "404 - Page Not Found"
msgstr ""
msgid "Internal Server Error (500)"
msgstr ""
msgid "API Connection Timeout"
msgstr ""
msgid "Invalid CSRF Token"
msgstr ""
msgid "Monday"
msgstr ""
msgid "Tuesday"
msgstr ""
msgid "Wednesday"
msgstr ""
msgid "Thursday"
msgstr ""
msgid "Friday"
msgstr ""
msgid "Saturday"
msgstr ""
msgid "Sunday"
msgstr ""
msgid "Just now"
msgstr ""
msgid "%(count)s minutes ago"
msgstr ""
msgid "Step 1 of 5"
msgstr ""
msgid "Skip tutorial"
msgstr ""
msgid "Next"
msgstr ""
msgid "Previous"
msgstr ""
msgid "Finish"
msgstr ""

37
demo.po Normal file
View File

@ -0,0 +1,37 @@
#
msgid ""
msgstr ""
"Project-Id-Version: Test Project 1.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-03-15 12:00+0000\n"
"PO-Revision-Date: \n"
"Last-Translator: \n"
"Language-Team: \n"
"Language: fr\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
msgid "Hello, world!"
msgstr "مرحبا، العالم!"
msgid "Welcome back, %s."
msgstr "مرحبًا بعودتك، %s."
msgid "User %(username)s has logged in."
msgstr "مستخدم %(username)s تم تسجيله دخولًا."
msgid "Please click <a href='%(url)s'>here</a> to reset your password."
msgstr ""
"رجاءً انقر على <a href='%(url)s'>هنا</a> للرجوع لكلمة المرور الخاصة بك."
msgid "Database connection failed: PostgreSQL error."
msgstr "فشل اتصال البيانات: خطأ PostgreSQL."
msgid "Good morning"
msgstr "صباح الخير"
msgctxt "button_label"
msgid "Save"
msgstr "حفظ"

167
demo1.po Normal file
View File

@ -0,0 +1,167 @@
#
msgid ""
msgstr ""
"Project-Id-Version: Big SaaS App 2.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-05-20 10:00+0000\n"
"PO-Revision-Date: \n"
"Last-Translator: \n"
"Language-Team: \n"
"Language: es\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
msgid "Dashboard"
msgstr "شاشة رئيسية"
msgid "My Profile"
msgstr "موقعي الشخصي"
msgid "Account Settings"
msgstr "إعدادات الحساب"
msgid "Billing & Invoices"
msgstr "إدارة الفواتير والضمان"
msgid "Log Out"
msgstr "تسجيل الخروج"
msgid "Email Address"
msgstr "عنوان البريد الإلكتروني"
msgid "Password"
msgstr "كلمة المرور"
msgid "Remember me on this device"
msgstr "تذكرني على هذا الجهاز"
msgid "Forgot your password?"
msgstr "هل فقدت كلمة المرور؟"
msgid "Don't have an account? Sign up."
msgstr "لا يوجد حساب؟ سجل."
msgid ""
"Ensure this field has at least %(limit_value)d characters (it has "
"%(show_value)d)."
msgstr ""
"تأكد من أن هذا الحقل لديه على الأقل %(limit_value)d من الأحرف (إنه لديه "
"%(show_value)d)."
msgctxt "noun"
msgid "Book"
msgstr "كتاب"
msgctxt "verb"
msgid "Book"
msgstr "كتاب"
msgctxt "month_name"
msgid "May"
msgstr "قد"
msgctxt "auxiliary_verb"
msgid "May"
msgstr "قد"
msgid "Product Description"
msgstr "وصف المنتج"
msgid "Add to Cart"
msgstr "إضافة إلى عربة التسوق"
msgid "Proceed to Checkout"
msgstr "التوجه إلى الدفع"
msgid "Total: $%(amount).2f"
msgstr "المجموع: $%(amount).2f"
msgid "Shipping Address"
msgstr "عنوان الشحن"
msgid "Order History"
msgstr "إدارة الطلبات"
msgid "Delete Account"
msgstr "حذف الحساب"
msgid "Save Changes"
msgstr "حفظ التغييرات"
msgid "Upload Avatar"
msgstr "تحميل صورة الملف الشخصي"
msgid ""
"Welcome to the platform. By using our services, you agree to our <a "
"href='%(terms_url)s'>Terms of Service</a> and <a "
"href='%(privacy_url)s'>Privacy Policy</a>."
msgstr ""
"مرحبًا بكم في المنصة. باستخدام خدماتنا، فإنك توافق على <a "
"href='%(terms_url)s'>Terms of Service</a> و <a "
"href='%(privacy_url)s'>Privacy Policy</a>."
msgid ""
"Please check your email inbox. We have sent a confirmation link to verify "
"your account ownership. The link will expire in 24 hours."
msgstr ""
"يرجى التحقق من صندوق بريدك الإلكتروني. قمنا بإرسال رابط التحقق من ملكية "
"حسابك. سيتم انتهاء هذا الرابط في 24 ساعة."
msgid "<strong>Warning:</strong> This action cannot be undone."
msgstr "تحذير: <strong>لا يمكن استعادتها.**"
msgid "404 - Page Not Found"
msgstr "404 - صفحة غير موجودة"
msgid "Internal Server Error (500)"
msgstr "خطأ داخلي (500)"
msgid "API Connection Timeout"
msgstr "وقت انقطاع الاتصال بـ API"
msgid "Invalid CSRF Token"
msgstr "توقيع CSRF غير صالح"
msgid "Monday"
msgstr "الاثنين"
msgid "Tuesday"
msgstr "الثلاثاء"
msgid "Wednesday"
msgstr "الأربعاء"
msgid "Thursday"
msgstr "الخميس"
msgid "Friday"
msgstr "الجمعة"
msgid "Saturday"
msgstr "السبت"
msgid "Sunday"
msgstr "الأحد"
msgid "Just now"
msgstr "الآن"
msgid "%(count)s minutes ago"
msgstr "%(count)s دقائق مضت"
msgid "Step 1 of 5"
msgstr "الخطوة الأولى من 5"
msgid "Skip tutorial"
msgstr "تجاهل التوثيق"
msgid "Next"
msgstr "التالي"
msgid "Previous"
msgstr "السابق"
msgid "Finish"
msgstr "الانتهاء"

10264
django.po Normal file

File diff suppressed because it is too large Load Diff

10252
django1.po Normal file

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

@ -934,38 +934,34 @@ class HiringAgencyForm(forms.ModelForm):
"name": forms.TextInput(
attrs={
"class": "form-control",
"placeholder": "Enter agency name",
"required": True,
}
),
"contact_person": forms.TextInput(
attrs={
"class": "form-control",
"placeholder": "Enter contact person name",
}
),
"email": forms.EmailInput(
attrs={"class": "form-control", "placeholder": "agency@example.com"}
attrs={"class": "form-control"}
),
"phone": forms.TextInput(
attrs={"class": "form-control", "placeholder": "+966 50 123 4567"}
attrs={"class": "form-control"}
),
"website": forms.URLInput(
attrs={"class": "form-control", "placeholder": "https://www.agency.com"}
attrs={"class": "form-control"}
),
"country": forms.Select(attrs={"class": "form-select"}),
"address": forms.Textarea(
attrs={
"class": "form-control",
"rows": 3,
"placeholder": "Enter agency address",
}
),
"notes": forms.Textarea(
attrs={
"class": "form-control",
"rows": 3,
"placeholder": "Internal notes about the agency",
}
),
}

View File

@ -0,0 +1,159 @@
import os
import json
import time
import polib
from concurrent.futures import ThreadPoolExecutor, as_completed
from django.core.management.base import BaseCommand
from django.conf import settings
from openai import OpenAI, APIConnectionError, RateLimitError
# Get API Key from settings or environment
API_KEY = "8319706a96014c5099b44057d231a154.YfbEMn17ZWXPudxK"
# API_KEY = getattr(settings, 'ZAI_API_KEY', os.environ.get('ZAI_API_KEY'))
class Command(BaseCommand):
help = 'Translate or fix fuzzy entries in a .po file using Z.ai (GLM) via OpenAI SDK'
def add_arguments(self, parser):
parser.add_argument('po_file_path', type=str, help='Path to the .po file')
parser.add_argument('--lang', type=str, help='Target language (e.g., "Chinese", "French")', required=True)
parser.add_argument('--batch-size', type=int, default=10, help='Entries per API call (default: 10)')
parser.add_argument('--workers', type=int, default=3, help='Concurrent threads (default: 3)')
parser.add_argument('--model', type=str, default="glm-4.6", help='Model version (default: glm-4.6)')
parser.add_argument('--fix-fuzzy', action='store_true', help='Include entries marked as fuzzy')
def handle(self, *args, **options):
if not API_KEY:
self.stderr.write(self.style.ERROR("Error: ZAI_API_KEY not found in settings or environment."))
return
# 1. Initialize Client based on your docs
client = OpenAI(
api_key=API_KEY,
#base_url="https://api.z.ai/api/paas/v4/"
base_url="https://api.z.ai/api/coding/paas/v4"
)
file_path = options['po_file_path']
target_lang = options['lang']
batch_size = options['batch_size']
max_workers = options['workers']
model_name = options['model']
fix_fuzzy = options['fix_fuzzy']
# 2. Load PO File
self.stdout.write(f"Loading {file_path}...")
try:
po = polib.pofile(file_path)
except Exception as e:
self.stderr.write(self.style.ERROR(f"Could not load file: {e}"))
return
# 3. Filter Entries
entries_to_process = []
for entry in po:
if entry.obsolete:
continue
if not entry.msgstr.strip() or (fix_fuzzy and 'fuzzy' in entry.flags):
entries_to_process.append(entry)
total = len(entries_to_process)
self.stdout.write(self.style.SUCCESS(f"Found {total} entries to process."))
if total == 0:
return
# 4. Batch Processing Logic
def chunked(iterable, n):
for i in range(0, len(iterable), n):
yield iterable[i:i + n]
batches = list(chunked(entries_to_process, batch_size))
self.stdout.write(f"Processing {len(batches)} batches with model {model_name}...")
# 5. Worker Function with Retry Logic
def process_batch(batch_entries):
texts = [e.msgid for e in batch_entries]
system_prompt = (
"You are a professional localization expert for a Django software project. "
"You will receive a JSON list of English strings. "
"Translate them accurately. "
"IMPORTANT Rules:\n"
"1. Return ONLY a JSON list of strings.\n"
"2. Preserve all Python variables (e.g. %(count)s, {name}, %s) exactly.\n"
"3. Do not translate HTML tags.\n"
"4. Do not explain, just return the JSON."
)
user_prompt = (
f"Translate these texts into {target_lang}:\n"
f"{json.dumps(texts, ensure_ascii=False)}"
)
# Simple retry loop for Rate Limits
attempts = 0
max_retries = 3
while attempts < max_retries:
try:
completion = client.chat.completions.create(
model=model_name,
messages=[
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_prompt}
],
temperature=0.1
)
content = completion.choices[0].message.content
# Sanitize markdown code blocks
content = content.replace('```json', '').replace('```', '').strip()
translations = json.loads(content)
if len(translations) != len(batch_entries):
return False, f"Mismatch: sent {len(batch_entries)}, got {len(translations)}"
# Update entries
for entry, trans in zip(batch_entries, translations):
entry.msgstr = trans
if 'fuzzy' in entry.flags:
entry.flags.remove('fuzzy')
return True, "Success"
except (RateLimitError, APIConnectionError) as e:
attempts += 1
wait_time = 2 ** attempts # Exponential backoff: 2s, 4s, 8s...
time.sleep(wait_time)
if attempts == max_retries:
return False, f"API Error after retries: {e}"
except json.JSONDecodeError:
return False, "AI returned invalid JSON"
except Exception as e:
return False, str(e)
# 6. Execution & Incremental Saving
success_count = 0
with ThreadPoolExecutor(max_workers=max_workers) as executor:
future_to_batch = {executor.submit(process_batch, batch): batch for batch in batches}
for i, future in enumerate(as_completed(future_to_batch)):
batch = future_to_batch[future]
success, msg = future.result()
if success:
success_count += len(batch)
self.stdout.write(self.style.SUCCESS(f"Batch {i+1}/{len(batches)} done."))
else:
self.stderr.write(self.style.WARNING(f"Batch {i+1} failed: {msg}"))
# Save every 5 batches to be safe
if (i + 1) % 5 == 0:
po.save()
self.stdout.write(f"--- Auto-saved at batch {i+1} ---")
# Final Save
po.save()
self.stdout.write(self.style.SUCCESS(f"\nComplete! Translated {success_count}/{total} entries."))

View File

@ -130,7 +130,8 @@ from .models import (
Message,
Document,
InterviewLocation,
InterviewNote
InterviewNote,
OnsiteLocationDetails
)
@ -211,12 +212,17 @@ class PersonDetailView(DetailView):
context_object_name = "person"
class PersonUpdateView(StaffRequiredMixin, UpdateView):
class PersonUpdateView( UpdateView):
model = Person
template_name = "people/update_person.html"
form_class = PersonForm
success_url = reverse_lazy("person_list")
def form_valid(self, form):
if self.request.POST.get("view") == "portal":
form.save()
return redirect("agency_portal_persons_list")
return super().form_valid(form)
class PersonDeleteView(StaffRequiredMixin, DeleteView):
model = Person
@ -3086,7 +3092,8 @@ def add_meeting_comment(request, slug):
"""Add a comment to a meeting"""
# from .forms import MeetingCommentForm
meeting = get_object_or_404(ZoomMeetingDetails, slug=slug)
meeting = get_object_or_404(InterviewNote, slug=slug)
print(meeting)
if request.method == "POST":
form = InterviewNoteForm(request.POST)
@ -3286,8 +3293,8 @@ def agency_create(request):
context = {
"form": form,
"title": "Create New Agency",
"button_text": "Create Agency",
"title": _("Create New Agency"),
"button_text": _("Create Agency"),
}
return render(request, "recruitment/agency_form.html", context)
@ -3344,7 +3351,7 @@ def agency_update(request, slug):
"form": form,
"agency": agency,
"title": f"Edit Agency: {agency.name}",
"button_text": "Update Agency",
"button_text": _("Update Agency"),
}
return render(request, "recruitment/agency_form.html", context)

268
run.py
View File

@ -1,44 +1,246 @@
import requests
import jwt
import os
import json
import time
import argparse
import polib
from concurrent.futures import ThreadPoolExecutor, as_completed
from openai import OpenAI, APIConnectionError, RateLimitError
ZOOM_API_KEY = 'OoDRW3uVTymEnnQWTZTsLQ'
ZOOM_API_SECRET = 'ZJ0hCFMrwekG71jbR3Trvoor4tK3HAVP'
# --- Terminal Colors ---
class Colors:
HEADER = '\033[95m'
BLUE = '\033[94m'
GREEN = '\033[92m'
WARNING = '\033[93m'
FAIL = '\033[91m'
ENDC = '\033[0m'
BOLD = '\033[1m'
def generate_zoom_jwt():
payload = {
'iss': ZOOM_API_KEY,
'exp': time.time() + 3600
}
token = jwt.encode(payload, ZOOM_API_SECRET, algorithm='HS256')
return token
def print_success(msg): print(f"{Colors.GREEN}{msg}{Colors.ENDC}")
def print_warning(msg): print(f"{Colors.WARNING}{msg}{Colors.ENDC}")
def print_error(msg): print(f"{Colors.FAIL}{msg}{Colors.ENDC}")
def print_info(msg): print(f"{Colors.BLUE}{msg}{Colors.ENDC}")
def create_zoom_meeting(topic, start_time, duration, host_email):
jwt_token = generate_zoom_jwt()
headers = {
'Authorization': f'Bearer {jwt_token}',
'Content-Type': 'application/json'
}
data = {
"topic": topic,
"type": 2,
"start_time": start_time,
"duration": duration,
"schedule_for": host_email,
"settings": {"join_before_host": True}
}
url = f"https://api.zoom.us/v2/users/{host_email}/meetings"
return requests.post(url, json=data, headers=headers)
# --- Provider Configurations ---
class ProviderFactory:
@staticmethod
def get_client(provider_name, api_key=None, base_url=None):
"""
Returns a configured OpenAI client and default model based on the provider.
"""
provider_name = provider_name.lower()
if provider_name == 'glm':
return OpenAI(
api_key=api_key or os.environ.get('ZAI_API_KEY'),
base_url="https://api.z.ai/api/coding/paas/v4/"
), "glm-4.6"
elif provider_name == 'ollama':
# Ollama acts like OpenAI but locally
return OpenAI(
api_key='ollama', # Required but ignored by Ollama
base_url=base_url or "http://localhost:11434/v1"
), "llama3" # Default model, user can override
elif provider_name == 'openai':
return OpenAI(
api_key=api_key or os.environ.get('OPENAI_API_KEY')
), "gpt-4o-mini"
else:
raise ValueError(f"Unknown provider: {provider_name}")
# --- Main Logic ---
def translate_po_file(args):
# 1. Setup Provider
try:
client, default_model = ProviderFactory.get_client(args.provider, args.api_key, args.api_base)
model_name = args.model or default_model
except Exception as e:
print_error(f"Configuration Error: {e}")
return
# 2. Load PO File
print_info(f"Loading file: {args.path}")
try:
po = polib.pofile(args.path)
except Exception as e:
print_error(f"Could not load file: {e}")
return
# 3. Filter Entries
entries_to_process = []
for entry in po:
if entry.obsolete:
continue
is_empty = not entry.msgstr.strip()
is_fuzzy = 'fuzzy' in entry.flags
if is_empty or (args.fix_fuzzy and is_fuzzy):
entries_to_process.append(entry)
total_entries = len(entries_to_process)
print_success(f"Target: {args.lang} | Provider: {args.provider} | Model: {model_name}")
print_success(f"Found {total_entries} entries to translate.")
if total_entries == 0:
print_success("Nothing to do.")
return
# 4. Special Handling for Local Ollama
# Local models struggle with concurrent requests. Force workers=1 unless overridden.
if args.provider == 'ollama' and args.workers > 1:
print_warning("⚠️ Warning: Using multiple workers with Ollama can crash local GPUs.")
print_warning(" Switching to workers=1 for stability (use --workers N to override).")
args.workers = 1
# 5. Batch Processing Helper
def chunked(iterable, n):
for i in range(0, len(iterable), n):
yield iterable[i:i + n]
batches = list(chunked(entries_to_process, args.batch_size))
# 6. Worker Function
def process_batch(batch_entries):
texts = [e.msgid for e in batch_entries]
# System prompt: Critical for JSON enforcement
system_prompt = (
"You are a professional technical translator. "
"You will receive a JSON list of English strings. "
"Translate them accurately. "
"IMPORTANT Rules:\n"
"1. Return ONLY a valid JSON list of strings.\n"
"2. Preserve python formatting (%(count)s, {name}, %s) exactly.\n"
"3. Do not translate HTML tags.\n"
"4. Do NOT output markdown (like ```json), just raw JSON."
)
user_prompt = (
f"Translate these texts into {args.lang}:\n"
f"{json.dumps(texts, ensure_ascii=False)}"
)
attempts = 0
max_retries = 3
while attempts < max_retries:
try:
completion = client.chat.completions.create(
model=model_name,
messages=[
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_prompt}
],
temperature=0.1, # Low temp for deterministic results
response_format={"type": "json_object"} if args.provider == "openai" else None
)
content = completion.choices[0].message.content
# Cleanup: Some local models love adding markdown blocks despite instructions
content = content.replace('```json', '').replace('```', '').strip()
# Flexible JSON parsing
try:
data = json.loads(content)
# Handle cases where model returns {"translations": [...]} instead of [...]
if isinstance(data, dict):
# Look for the first list value
found_list = False
for v in data.values():
if isinstance(v, list):
translations = v
found_list = True
break
if not found_list:
return False, f"Could not find list in JSON object: {data}"
else:
translations = data
except json.JSONDecodeError:
return False, f"Invalid JSON received: {content[:50]}..."
if not isinstance(translations, list) or len(translations) != len(batch_entries):
return False, f"Count mismatch: sent {len(batch_entries)}, got {len(translations) if isinstance(translations, list) else 'invalid'}"
# Apply translations with Type Checking
for entry, translation in zip(batch_entries, translations):
# --- VITAL FIX FOR AttributeError: 'dict' object has no attribute 'splitlines' ---
if isinstance(translation, dict):
# Tries to grab the first string value found in the dict (e.g., {"text": "Hello"})
extracted = next((str(v) for v in translation.values() if isinstance(v, str)), None)
translation = extracted if extracted else str(translation)
elif not isinstance(translation, str):
# Ensure all other types (like boolean or int) are converted to string
translation = str(translation)
# ---------------------------------------------------------------------------------
entry.msgstr = translation
if 'fuzzy' in entry.flags:
entry.flags.remove('fuzzy')
return True, "Success"
except (RateLimitError, APIConnectionError) as e:
attempts += 1
time.sleep(2 ** attempts) # Exponential backoff
if attempts == max_retries:
return False, f"API Error: {e}"
except Exception as e:
return False, f"Unexpected: {e}"
# 7. Execution Loop
success_count = 0
print_info(f"Starting processing {len(batches)} batches...")
with ThreadPoolExecutor(max_workers=args.workers) as executor:
future_to_batch = {executor.submit(process_batch, batch): batch for batch in batches}
for i, future in enumerate(as_completed(future_to_batch)):
batch = future_to_batch[future]
success, msg = future.result()
if success:
success_count += len(batch)
print_success(f"[{i+1}/{len(batches)}] Batch done.")
else:
print_warning(f"[{i+1}/{len(batches)}] Batch failed: {msg}")
# Auto-save every 5 batches
if (i + 1) % 5 == 0:
po.save()
# 8. Final Save
po.save()
print_success(f"\n------------------------------------------------")
print_success(f"Finished! Translated {success_count}/{total_entries} entries.")
print_success(f"File saved: {args.path}")
if __name__ == "__main__":
topic = "Your Meeting Topic"
start_time = "2023-08-25T10:00:00"
duration = 60
host_email = "your_zoom_email"
response = create_zoom_meeting(topic, start_time, duration, host_email)
print(response.json())
parser = argparse.ArgumentParser(description="Translate .po files using AI Providers (Z.ai, Ollama, OpenAI)")
parser.add_argument('path', type=str, help='Path to the .po file')
parser.add_argument('--lang', type=str, required=True, help='Target language (e.g., "French", "zh-CN")')
# Provider Settings
parser.add_argument('--provider', type=str, default='glm', choices=['glm', 'ollama', 'openai'], help='AI Provider to use')
parser.add_argument('--model', type=str, help='Model name (e.g., glm-4, llama3, gpt-4). Defaults vary by provider.')
parser.add_argument('--api-key', type=str, help='API Key (optional if env var is set)')
parser.add_argument('--api-base', type=str, help='Custom API Base URL (useful for custom Ollama ports)')
# Performance Settings
parser.add_argument('--batch-size', type=int, default=10, help='Lines per request. Keep low (5-10) for local models.')
parser.add_argument('--workers', type=int, default=3, help='Parallel threads. Note: Ollama defaults to 1.')
# Logic Settings
parser.add_argument('--fix-fuzzy', action='store_true', help='Re-translate entries marked as fuzzy')
args = parser.parse_args()
translate_po_file(args)

View File

@ -144,17 +144,6 @@
</a>
</li>
<li class="nav-item ms-3">
{% if request.user.is_authenticated %}
<form method="post" action="{% url 'account_logout' %}" class="d-inline py-2 d-flex align-items-center">
{% csrf_token %}
<button type="submit" class="btn btn-outline-light btn-sm">
<i class="fas fa-sign-out-alt me-1"></i> {% trans "Logout" %}
</button>
</form>
{% endif %}
</li>
<li class="nav-item dropdown">
<button
class="nav-link p-0 border-0 bg-transparent dropdown-toggle"

View File

@ -219,6 +219,11 @@
<h5 class="text-muted mb-3">{% trans "Administrative & Location" %}
<a href="{% url 'job_update' job.slug %}" class="btn btn-main-action btn-sm"><li class="fa fa-edit"></li>{% trans "Edit JOb" %}</a>
<div class="float-end">
<div class="d-flex align-items-center">
<i class="fas fa-user-tie me-2 text-primary"></i> <strong>{% trans "Assigned to :" %} </strong> {{ job.assigned_to|default:"N/A" }}
</div>
</div>
</h5>
<div class="row g-3 mb-4 border-bottom pb-3 small text-secondary">
<div class="col-md-6">
@ -363,20 +368,16 @@
{% else %}
{% if job.form_template.is_active %}
<a href="{% url 'application_submit_form' job.form_template.slug %}" class="btn btn-outline-secondary w-100">
<i class="fas fa-list-alt me-1"></i> {% trans "View Form Template" %}
<i class="fas fa-eye me-1"></i> {% trans "View" %}
</a>
<a href="{% url 'form_builder' job.form_template.slug %}" class="btn btn-outline-secondary w-100">
<i class="fas fa-list-alt me-1"></i> {% trans "Manage Form Template" %}
<i class="fas fa-list-alt me-1"></i> {% trans "Manage" %}
</a>
{% else %}
<p>{% trans "This job status is not active, the form will appear once the job is made active"%}</p>
{% endif %}
{% endif %}
</div>
</div>

View File

@ -287,7 +287,7 @@
<td><strong class="text-primary"><a href="" class="text-decoration-none text-secondary">{{ meeting.topic }}<a></strong></td>
<td>
{# Display the event type badge #}
<span class="status-badge bg-{{ meeting.type }}">{{ meeting.type|title }}</span>
<span class="status-badge bg-primary-theme text-white">{{ meeting.type|title }}</span>
</td>
<td>
<a class="text-primary text-decoration-none" href="{% url 'candidate_detail' meeting.interview.application.person.slug %}">{{ meeting.interview.application.person.full_name }} <i class="fas fa-link"></i></a>
@ -299,7 +299,7 @@
<td>{{ meeting.duration }} min</td>
<td>
{# Display the meeting status badge from the ScheduledInterview model #}
<span class="status-badge bg-{{ meeting.status }}">
<span class="status-badge bg-primary-theme text-white">
{{ meeting.interview.get_status_display }}
</span>
</td>

View File

@ -377,7 +377,7 @@ body {
{# ========================================================= #}
<div class="row g-4 mt-1">
<div class="col-lg-12">
{% comment %} <div class="col-lg-12">
<div class="card" id="comments-card" style="height: 100%;">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="card-title mb-0" style="color: var(--kaauh-teal-dark);">
@ -386,7 +386,6 @@ body {
</h5>
</div>
<div class="card-body overflow-auto">
{# 1. COMMENT DISPLAY & IN-PAGE EDIT FORMS #}
<div id="comment-section" class="mb-4">
{% if meeting.comments.all %}
@ -471,7 +470,7 @@ body {
{% endif %}
</div>
</div>
</div>
</div> {% endcomment %}
</div>
</div>
@ -487,16 +486,9 @@ body {
<form method="post" action="{% url 'create_interview_participants' meeting.interview.slug %}">
{% csrf_token %}
<div class="modal-body table-responsive">
{{ meeting.name }}
<hr>
<table class="table tab table-bordered mt-3">
<thead>
<th class="col">👥 {% trans "Participants" %}</th>
@ -514,10 +506,7 @@ body {
{{ form.system_users }}
</td>
</tr>
</table>
</div>
<div class="modal-footer">

View File

@ -252,147 +252,15 @@
{% endfor %}
{% endif %}
<form method="post" enctype="multipart/form-data" id="person-form">
<form method="post" action="{% url 'person_update' person.slug %}" enctype="multipart/form-data" id="person-form">
{% csrf_token %}
<!-- Profile Image Section -->
<div class="row mb-4">
<div class="col-12">
<div class="profile-image-upload" onclick="document.getElementById('id_profile_image').click()">
<div id="image-preview-container">
{% if person.profile_image %}
<img src="{{ person.profile_image.url }}" alt="Current Profile"
class="profile-image-preview">
<h5 class="text-muted mt-3">{% trans "Click to change photo" %}</h5>
<p class="text-muted small">{% trans "Current photo will be replaced" %}</p>
{% else %}
<i class="fas fa-camera fa-3x text-muted mb-3"></i>
<h5 class="text-muted">{% trans "Upload Profile Photo" %}</h5>
<p class="text-muted small">{% trans "Click to browse or drag and drop" %}</p>
{% endif %}
</div>
<input type="file" name="profile_image" id="id_profile_image"
class="d-none" accept="image/*">
</div>
{% if person.profile_image %}
<div class="mt-2">
<small class="text-muted">
{% trans "Leave empty to keep current photo" %}
</small>
</div>
{% endif %}
</div>
</div>
<!-- Personal Information Section -->
<div class="row mb-4">
<div class="col-12">
<h5 class="mb-3" style="color: var(--kaauh-teal-dark); border-bottom: 2px solid var(--kaauh-teal); padding-bottom: 0.5rem;">
<i class="fas fa-user me-2"></i> {% trans "Personal Information" %}
</h5>
</div>
<div class="col-md-4">
{{ form.first_name|as_crispy_field }}
</div>
<div class="col-md-4">
{{ form.middle_name|as_crispy_field }}
</div>
<div class="col-md-4">
{{ form.last_name|as_crispy_field }}
</div>
</div>
<!-- Contact Information Section -->
<div class="row mb-4">
<div class="col-12">
<h5 class="mb-3" style="color: var(--kaauh-teal-dark); border-bottom: 2px solid var(--kaauh-teal); padding-bottom: 0.5rem;">
<i class="fas fa-envelope me-2"></i> {% trans "Contact Information" %}
</h5>
</div>
<div class="col-md-6">
{{ form.email|as_crispy_field }}
</div>
<div class="col-md-6">
{{ form.phone|as_crispy_field }}
</div>
</div>
<!-- Additional Information Section -->
<div class="row mb-4">
<div class="col-12">
<h5 class="mb-3" style="color: var(--kaauh-teal-dark); border-bottom: 2px solid var(--kaauh-teal); padding-bottom: 0.5rem;">
<i class="fas fa-info-circle me-2"></i> {% trans "Additional Information" %}
</h5>
</div>
<div class="col-md-4">
{{ form.date_of_birth|as_crispy_field }}
</div>
<div class="col-md-4">
{{ form.nationality|as_crispy_field }}
</div>
<div class="col-md-4">
{{ form.gender|as_crispy_field }}
</div>
</div>
<!-- Address Section -->
<div class="row mb-4">
<div class="col-12">
<h5 class="mb-3" style="color: var(--kaauh-teal-dark); border-bottom: 2px solid var(--kaauh-teal); padding-bottom: 0.5rem;">
<i class="fas fa-map-marker-alt me-2"></i> {% trans "Address Information" %}
</h5>
</div>
<div class="col-12">
{{ form.address|as_crispy_field }}
</div>
</div>
<!-- LinkedIn Profile Section -->
<div class="row mb-4">
<div class="col-12">
<h5 class="mb-3" style="color: var(--kaauh-teal-dark); border-bottom: 2px solid var(--kaauh-teal); padding-bottom: 0.5rem;">
<i class="fab fa-linkedin me-2"></i> {% trans "Professional Profile" %}
</h5>
</div>
<div class="col-12">
<div class="form-group mb-3">
<label for="id_linkedin_profile" class="form-label">
{% trans "LinkedIn Profile URL" %}
</label>
<input type="url" name="linkedin_profile" id="id_linkedin_profile"
class="form-control" placeholder="https://linkedin.com/in/username"
value="{{ person.linkedin_profile|default:'' }}">
<small class="form-text text-muted">
{% trans "Optional: Add LinkedIn profile URL" %}
</small>
</div>
</div>
</div>
<!-- Form Actions -->
<div class="row">
<div class="col-12">
<div class="d-flex justify-content-between align-items-center">
<div class="d-flex gap-2">
<a href="{% url 'person_detail' person.slug %}" class="btn btn-outline-secondary">
<i class="fas fa-times me-1"></i> {% trans "Cancel" %}
</a>
<a href="{% url 'person_list' %}" class="btn btn-outline-secondary">
<i class="fas fa-list me-1"></i> {% trans "Back to List" %}
</a>
</div>
<div class="d-flex gap-2">
<button type="reset" class="btn btn-outline-secondary">
<i class="fas fa-undo me-1"></i> {% trans "Reset Changes" %}
</button>
<button type="submit" class="btn btn-main-action">
<i class="fas fa-save me-1"></i> {% trans "Update Applicant" %}
</button>
</div>
</div>
</div>
</div>
{{form|crispy}}
</form>
<div class="d-flex gap-2">
<button form="person-form" type="submit" class="btn btn-main-action">
<i class="fas fa-save me-1"></i> {% trans "Update" %}
</button>
</div>
</div>
</div>
</div>

View File

@ -73,8 +73,25 @@
</button>
<div class="collapse navbar-collapse" id="agencyNavbar">
<div class="navbar-nav ms-auto">
<div class="navbar-nav ms-auto">
<li class="nav-item me-2">
{% if LANGUAGE_CODE == 'en' %}
<form action="{% url 'set_language' %}" method="post" class="d-inline">{% csrf_token %}
<input name="next" type="hidden" value="{{ request.get_full_path }}">
<button name="language" value="ar" class="btn bg-primary-theme text-white" type="submit">
<span class="me-2">🇸🇦</span> العربية
</button>
</form>
{% elif LANGUAGE_CODE == 'ar' %}
<form action="{% url 'set_language' %}" method="post" class="d-inline">{% csrf_token %}
<input name="next" type="hidden" value="{{ request.get_full_path }}">
<button name="language" value="en" class="btn bg-primary-theme text-white" type="submit">
<span class="me-2">🇺🇸</span> English
</button>
</form>
{% endif %}
</li>
{# NAVIGATION LINKS (Add your portal links here if needed) #}
{% if request.user.user_type == 'agency' %}
<li class="nav-item">
@ -113,7 +130,7 @@
</li>
<li class="nav-item dropdown">
{% comment %} <li class="nav-item dropdown">
<a class="nav-link dropdown-toggle text-white" href="#" role="button" data-bs-toggle="dropdown"
data-bs-offset="0, 8" aria-expanded="false" aria-label="{% trans 'Toggle language menu' %}">
<i class="fas fa-globe me-1"></i>
@ -138,6 +155,16 @@
</li>
</ul>
</li>
{% if request.user.is_authenticated %}
<li class="nav-item">
<a class="nav-link text-white" href="{% url 'user_detail' request.user.pk %}">
<i class="fas fa-user-circle me-1"></i> <span>{% trans "My Profile" %}</span></a></li>
{% endif %}
<li class="nav-item me-2">
<a class="nav-link text-white" href="{% url 'message_list' %}">
<i class="fas fa-envelope"></i> <span>{% trans "Messages" %}</span>
</a>
</li>
<li class="nav-item ms-3">
{% if request.user.is_authenticated %}
@ -250,6 +277,21 @@
} catch(e) {
console.error(e);
}
function closeOpenBootstrapModal() {
const openModalElement = document.querySelector('.modal.show');
if (openModalElement) {
const modal = bootstrap.Modal.getInstance(openModalElement);
if (modal) {
modal.hide();
} else {
console.warn("Found an open modal element, but could not get the Bootstrap Modal instance.");
}
} else {
console.log("No open Bootstrap Modal found to close.");
}
}
</script>

View File

@ -238,11 +238,13 @@
<table class="table table-hover">
<thead>
<tr>
<th>{% trans "Name" %}</th>
<th>{% trans "Contact" %}</th>
<th>{% trans "Stage" %}</th>
<th>{% trans "Submitted" %}</th>
<th>{% trans "Actions" %}</th>
<th class="px-4 py-3 text-uppercase small fw-bold text-muted">{% trans "Candidate"%}</th>
<th class="px-4 py-3 text-uppercase small fw-bold text-muted">{% trans "Contact" %}
</th>
<th class="px-4 py-3 text-uppercase small fw-bold text-muted">{% trans "Stage" %}
</th>
<th class="px-4 py-3 text-uppercase small fw-bold text-muted">{% trans "Submitted"%}</th>
<th class="px-4 py-3 text-uppercase small fw-bold text-muted text-end">{% trans "Actions" %}</th>
</tr>
</thead>
<tbody>
@ -262,10 +264,19 @@
</td>
<td>
<div class="small text-muted">
{{ candidate.created_at|date:"Y-m-d H:i" }}
<div class="mb-1"><i class="fas fa-envelope me-2 w-20"></i>
{{candidate.email }}</div>
<div><i class="fas fa-phone me-2 w-20"></i>{{ candidate.phone }}</div>
</div>
</td>
<td>
<td class="px-4">
<span class="badge bg-soft-info text-info rounded-pill px-3">
{{candidate.get_stage_display }}</span>
</td>
<td class="px-4">
<span class="small text-muted">{{ candidate.created_at|date:"M d, Y" }}</span>
</td>
<td class="px-4 text-end">
<a href="{% url 'candidate_detail' candidate.slug %}"
class="btn btn-sm btn-outline-primary" title="{% trans 'View Details' %}">
<i class="fas fa-eye"></i>
@ -315,9 +326,9 @@
cy="60"
style="stroke-dasharray: 326.73; stroke-dashoffset: {{ stroke_dashoffset }};"/>
</svg>
<div class="progress-ring-text">
{% widthratio total_candidates assignment.max_candidates 100 as progress %}
{{ progress|floatformat:0 }}%
<div class="position-absolute top-50 start-50 translate-middle text-center">
<div class="h3 fw-bold mb-0 text-dark">{{ total_candidates }}</div>
<div class="small text-muted text-uppercase">{% trans "of" %} {{ assignment.max_candidates}}</div>
</div>
</div>
</div>

View File

@ -146,7 +146,7 @@
<div class="mb-3">
<label class="text-muted small">{% trans "Status" %}</label>
<div>
<span class="status-badge status-{{ assignment.status }}">
<span class="status-badge status-{{ assignment.status }}" >
{{ assignment.get_status_display }}
</span>
</div>
@ -267,12 +267,6 @@
<a href="{% url 'candidate_application_detail' candidate.slug %}" class="btn btn-sm btn-outline-primary" title="{% trans 'View Profile' %}">
<i class="fas fa-eye"></i>
</a>
<button class="btn btn-sm btn-outline-primary" onclick="editCandidate({{ candidate.id }})" title="{% trans 'Edit Candidate' %}">
<i class="fas fa-edit"></i>
</button>
<button class="btn btn-sm btn-outline-danger" onclick="deleteCandidate({{ candidate.id }}, '{{ candidate.name }}')" title="{% trans 'Remove Candidate' %}">
<i class="fas fa-trash"></i>
</button>
</td>
</tr>
{% endfor %}

View File

@ -147,7 +147,7 @@
<div class="kaauh-card shadow-sm">
<div class="card-body p-0">
{% if page_obj %}
<div class="table-responsive">
<div class="table-responsive person-table">
<table class="table table-hover mb-0">
<thead class="table-light">
<tr>
@ -155,8 +155,8 @@
<th scope="col">{% trans "Email" %}</th>
<th scope="col">{% trans "Phone" %}</th>
<th scope="col">{% trans "Job" %}</th>
<th scope="col">{% trans "Stage" %}</th>
<th scope="col">{% trans "Applied Date" %}</th>
{% comment %} <th scope="col">{% trans "Stage" %}</th>
<th scope="col">{% trans "Applied Date" %}</th> {% endcomment %}
<th scope="col" class="text-center">{% trans "Actions" %}</th>
</tr>
</thead>
@ -183,7 +183,7 @@
</a>
</td>
<td>{{ person.phone|default:"-" }}</td>
<td>
{% comment %} <td>
<span class="badge bg-light text-dark">
{{ person.job.title|truncatechars:30 }}
</span>
@ -201,19 +201,19 @@
{{ person.get_stage_display }}
</span>
{% endwith %}
</td>
</td> {% endcomment %}
<td>{{ person.created_at|date:"Y-m-d" }}</td>
<td class="text-center">
<div class="btn-group" role="group">
<a href="{% url 'candidate_detail' person.slug %}"
class="btn btn-sm btn-outline-primary"
title="{% trans 'View Details' %}">
<i class="fas fa-eye"></i>
</a>
<button type="button"
<button type="button" data-bs-toggle="modal" data-bs-target="#updateModal"
hx-get="{% url 'person_update' person.slug %}"
hx-target="#updateModalBody"
hx-swap="outerrHTML"
hx-select="#person-form"
hx-vals='{"view":"portal"}'
class="btn btn-sm btn-outline-secondary"
title="{% trans 'Edit Person' %}"
onclick="editPerson({{ person.id }})">
>
<i class="fas fa-edit"></i>
</button>
</div>
@ -290,6 +290,28 @@
</nav>
{% endif %}
</div>
<!-- Empty Modal -->
<div class="modal fade" id="updateModal" tabindex="-1" aria-labelledby="updateModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content" hx-boost="true" hx-vals='{"view":"portal"}' hx-select=".person-table" hx-target=".person-table"
hx-swap="outerHTML" hx-on::after-request="closeOpenBootstrapModal()">
<div class="modal-header">
<h5 class="modal-title" id="updateModalLabel">{% trans "Update" %}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body" id="updateModalBody">
</div>
<div class="modal-footer">
<div class="d-flex gap-2">
<button form="person-form" type="submit" class="btn btn-main-action">
<i class="fas fa-save me-1"></i> {% trans "Update" %}
</button>
</div>
</div>
</div>
</div>
</div>
<!-- Person Modal -->
<div class="modal fade modal-lg" id="personModal" tabindex="-1" aria-labelledby="personModalLabel" aria-hidden="true">
<div class="modal-dialog">

View File

@ -573,25 +573,10 @@
</div>
{% endif %}
{% comment %} <div class="col-md-3 mb-3">
<div class="kaauh-card h-100">
<div class="card-body text-center">
<i class="fas fa-edit fa-2x text-warning mb-3"></i>
<h6>{% trans "Update Profile" %}</h6>
<p class="text-muted small">{% trans "Edit your personal information" %}</p>
<a href="" class="btn btn-main-action w-100">
<i class="fas fa-edit me-2"></i>
{% trans "Update" %}
</a>
</div>
</div>
</div> {% endcomment %}
</div>
<!-- Next Steps Section -->
{% comment %} <div class="row">
<div class="row">
<div class="col-12">
<div class="kaauh-card">
<div class="card-header bg-primary-theme text-white">

View File

@ -291,7 +291,7 @@
hx-get="{% url 'candidate_criteria_view_htmx' candidate.pk %}"
hx-target="#candidateviewModalBody"
title="View Profile">
{{ candidate.name }} <i class="fas fa-eye ms-1"></i>
{{ candidate.name }}
</button>
{% comment %} <div class="candidate-name">
{{ candidate.name }}

View File

@ -1,7 +1,7 @@
{% extends "base.html" %}
{% load static %}
{% load static i18n %}
{% block title %}{{ source.name }} - Source Details{% endblock %}
{% block title %}{{ source.name }} - {% trans "Source Details" %}{% endblock %}
{% block content %}
<div class="container-fluid">
@ -11,7 +11,7 @@
<h1 class="h3 mb-0">{{ source.name }}</h1>
<div class="btn-group">
<a href="{% url 'source_update' source.pk %}" class="btn btn-outline-secondary">
<i class="fas fa-edit"></i> Edit
<i class="fas fa-edit"></i> {% trans "Edit" %}
</a>
{% comment %} <a href="{% url 'generate_api_keys' source.pk %}" class="btn btn-warning">
<i class="fas fa-key"></i> Generate Keys
@ -24,13 +24,12 @@
hx-select="#toggle-source-status"
hx-select-oob="#source-status"
hx-swap="outerHTML"
hx-confirm="Are you sure you want to {{ source.is_active|yesno:'deactivate,activate' }} this source?"
title="{{ source.is_active|yesno:'Deactivate,Activate' }}">
hx-confirm="{% blocktrans %}Are you sure you want to {{ source.is_active|yesno:'deactivate,activate' }} this source?{% endblocktrans %}">
<i class="fas fa-{{ source.is_active|yesno:'pause,play' }}"></i>
{{ source.is_active|yesno:'Deactivate,Activate' }}
</button>
<a href="{% url 'source_delete' source.pk %}" class="btn btn-outline-danger">
<i class="fas fa-trash"></i> Delete
<i class="fas fa-trash"></i> {% trans "Delete" %}
</a>
</div>
</div>
@ -40,19 +39,19 @@
<div class="col-md-8">
<div class="card mb-4">
<div class="card-header">
<h6 class="mb-0">Source Information</h6>
<h6 class="mb-0">{% trans "Source Information" %}</h6>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-6">
<div class="mb-3">
<label class="form-label text-muted">Name</label>
<label class="form-label text-muted">{% trans "Name" %}</label>
<div class="fw-bold">{{ source.name }}</div>
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label class="form-label text-muted">Type</label>
<label class="form-label text-muted">{% trans "Type" %}</label>
<div>
<span class="badge bg-info">{{ source.get_source_type_display }}</span>
</div>
@ -62,7 +61,7 @@
{% if source.description %}
<div class="mb-3">
<label class="form-label text-muted">Description</label>
<label class="form-label text-muted">{{ _("Description") }}</label>
<div>{{ source.description|linebreaks }}</div>
</div>
{% endif %}
@ -70,24 +69,24 @@
<div class="row">
<div class="col-md-6">
<div class="mb-3">
<label class="form-label text-muted">Contact Email</label>
<label class="form-label text-muted">{{ _("Contact Email") }}</label>
<div>
{% if source.contact_email %}
<a href="mailto:{{ source.contact_email }}">{{ source.contact_email }}</a>
{% else %}
<span class="text-muted">Not specified</span>
<span class="text-muted">{{ _("Not specified") }}</span>
{% endif %}
</div>
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label class="form-label text-muted">Contact Phone</label>
<label class="form-label text-muted">{{ _("Contact Phone") }}</label>
<div>
{% if source.contact_phone %}
{{ source.contact_phone }}
{% else %}
<span class="text-muted">Not specified</span>
<span class="text-muted">{{ _("Not specified") }}</span>
{% endif %}
</div>
</div>
@ -97,24 +96,24 @@
<div class="row">
<div class="col-md-6">
<div class="mb-3">
<label class="form-label text-muted">Status</label>
<label class="form-label text-muted">{{ _("Status") }}</label>
<div id="source-status">
{% if source.is_active %}
<span class="badge bg-success">Active</span>
<span class="badge bg-success">{{ _("Active") }}</span>
{% else %}
<span class="badge bg-secondary">Inactive</span>
<span class="badge bg-secondary">{{ _("Inactive") }}</span>
{% endif %}
</div>
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label class="form-label text-muted">Requires Authentication</label>
<label class="form-label text-muted">{% trans "Requires Authentication" %}</label>
<div>
{% if source.requires_auth %}
<span class="badge bg-warning">Yes</span>
<span class="badge bg-warning">{% trans "Yes" %}</span>
{% else %}
<span class="badge bg-secondary">No</span>
<span class="badge bg-secondary">{% trans "No" %}</span>
{% endif %}
</div>
</div>
@ -123,21 +122,21 @@
{% if source.webhook_url %}
<div class="mb-3">
<label class="form-label text-muted">Webhook URL</label>
<label class="form-label text-muted">{% trans "Webhook URL" %}</label>
<div><code>{{ source.webhook_url }}</code></div>
</div>
{% endif %}
{% if source.api_timeout %}
<div class="mb-3">
<label class="form-label text-muted">API Timeout</label>
<div>{{ source.api_timeout }} seconds</div>
<label class="form-label text-muted">{{ _("API Timeout") }}</label>
<div>{{ source.api_timeout }} {{ _("seconds") }}</div>
</div>
{% endif %}
{% if source.notes %}
<div class="mb-3">
<label class="form-label text-muted">Notes</label>
<label class="form-label text-muted">{{ _("Notes") }}</label>
<div>{{ source.notes|linebreaks }}</div>
</div>
{% endif %}
@ -145,13 +144,13 @@
<div class="row">
<div class="col-md-6">
<div class="mb-3">
<label class="form-label text-muted">Created</label>
<label class="form-label text-muted">{{ _("Created") }}</label>
<div>{{ source.created_at|date:"M d, Y H:i" }}</div>
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label class="form-label text-muted">Last Updated</label>
<label class="form-label text-muted">{{ _("Last Updated") }}</label>
<div>{{ source.updated_at|date:"M d, Y H:i" }}</div>
</div>
</div>
@ -164,23 +163,23 @@
<!-- API Credentials -->
<div class="card mb-4">
<div class="card-header">
<h6 class="mb-0">API Credentials</h6>
<h6 class="mb-0">{% trans "API Credentials" %}</h6>
</div>
<div id="api-credentials" class="card-body">
<div class="mb-3">
<label class="form-label text-muted">API Key</label>
<label class="form-label text-muted">{% trans "API Key" %}</label>
<div class="input-group">
<input type="text" class="form-control" value="{{ source.api_key }}" readonly>
<button type="button" class="btn btn-outline-secondary"
hx-post="{% url 'copy_to_clipboard' %}"
hx-vals='{"text": "{{ source.api_key }}"}'
title="Copy to clipboard">
title="{% trans "Copy to clipboard" %}">
<i class="fas fa-copy"></i>
</button>
</div>
</div>
<div class="mb-3">
<label class="form-label text-muted">API Secret</label>
<label class="form-label text-muted">{% trans "API Secret" %}</label>
<div class="input-group">
<input type="password" class="form-control" value="{{ source.api_secret }}" readonly id="api-secret">
<button type="button" class="btn btn-outline-secondary" onclick="toggleSecretVisibility()">
@ -189,14 +188,14 @@
<button type="button" class="btn btn-outline-secondary"
hx-post="{% url 'copy_to_clipboard' %}"
hx-vals='{"text": "{{ source.api_secret }}"}'
title="Copy to clipboard">
title="{% trans "Copy to clipboard" %}">
<i class="fas fa-copy"></i>
</button>
</div>
</div>
<div class="text-end">
<a hx-post="{% url 'generate_api_keys' source.pk %}" hx-target="#api-credentials" hx-select="#api-credentials" hx-swap="outerHTML" class="btn btn-main-action btn-sm">
<i class="fas fa-key"></i> Generate New Keys
<i class="fas fa-key"></i> {% trans "Generate New Keys" %}
</a>
</div>
</div>
@ -205,24 +204,24 @@
<!-- Statistics -->
<div class="card mb-4">
<div class="card-header">
<h6 class="mb-0">Integration Statistics</h6>
<h6 class="mb-0">{% trans "Integration Statistics" %}</h6>
</div>
<div class="card-body">
<div class="mb-3">
<label class="form-label text-muted">Total API Calls</label>
<label class="form-label text-muted">{% trans "Total API Calls" %}</label>
<div class="h5 mb-0">{{ total_logs }}</div>
</div>
<div class="mb-3">
<label class="form-label text-muted">Successful Calls</label>
<label class="form-label text-muted">{% trans "Successful Calls" %}</label>
<div class="h5 mb-0 text-success">{{ successful_logs }}</div>
</div>
<div class="mb-3">
<label class="form-label text-muted">Failed Calls</label>
<label class="form-label text-muted">{% trans "Failed Calls" %}</label>
<div class="h5 mb-0 text-danger">{{ failed_logs }}</div>
</div>
{% if total_logs > 0 %}
<div class="mb-3">
<label class="form-label text-muted">Success Rate</label>
<label class="form-label text-muted">{% trans "Success Rate" %}</label>
<div class="h5 mb-0">
{% widthratio successful_logs total_logs 100 %}%
</div>
@ -236,8 +235,8 @@
<!-- Integration Logs -->
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<h6 class="mb-0">Recent Integration Logs</h6>
<small class="text-muted">Last 10 logs</small>
<h6 class="mb-0">{% trans "Recent Integration Logs" %}</h6>
<small class="text-muted">{% trans "Last 10 logs" %}</small>
</div>
<div class="card-body">
{% if integration_logs %}
@ -245,11 +244,11 @@
<table class="table table-sm">
<thead class="table-light">
<tr>
<th>Timestamp</th>
<th>Method</th>
<th>Status</th>
<th>Response Time</th>
<th>Details</th>
<th>{% trans "Timestamp" %}</th>
<th>{% trans "Method" %}</th>
<th>{% trans "Status" %}</th>
<th>{% trans "Response Time" %}</th>
<th>{% trans "Details" %}</th>
</tr>
</thead>
<tbody>
@ -286,7 +285,7 @@
<i class="fas fa-eye"></i>
</button>
{% else %}
<span class="text-muted">No data</span>
<span class="text-muted">{{ _("No data") }}</span>
{% endif %}
</td>
</tr>
@ -297,7 +296,7 @@
{% else %}
<div class="text-center py-4">
<i class="fas fa-clipboard-list fa-2x text-muted mb-3"></i>
<p class="text-muted">No integration logs found</p>
<p class="text-muted">{{ _("No integration logs found") }}</p>
</div>
{% endif %}
</div>
@ -313,24 +312,24 @@
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Integration Log Details</h5>
<h5 class="modal-title">{% trans "Integration Log Details" %}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div class="row">
<div class="col-md-6">
<strong>Timestamp:</strong><br>
<strong>{{ _("Timestamp:") }}:</strong><br>
{{ log.created_at|date:"M d, Y H:i:s" }}
</div>
<div class="col-md-6">
<strong>Method:</strong><br>
<strong>{{ _("Method:") }}:</strong><br>
<span class="badge bg-secondary">{{ log.method }}</span>
</div>
</div>
<hr>
<div class="row">
<div class="col-md-6">
<strong>Status Code:</strong><br>
<strong>{{ _("Status Code:") }}:</strong><br>
{% if log.status_code >= 200 and log.status_code < 300 %}
<span class="badge bg-success">{{ log.status_code }}</span>
{% elif log.status_code >= 400 %}
@ -340,7 +339,7 @@
{% endif %}
</div>
<div class="col-md-6">
<strong>Response Time:</strong><br>
<strong>{{ _("Response Time:") }}:</strong><br>
{% if log.response_time_ms %}
{{ log.response_time_ms }}ms
{% else %}
@ -350,25 +349,24 @@
</div>
<hr>
<div class="mb-3">
<strong>Request Data:</strong>
<strong>{{ _("Request Data:") }}:</strong>
<pre class="bg-light p-2 rounded"><code>{{ log.request_data|pprint }}</code></pre>
</div>
{% if log.response_data %}
<div class="mb-3">
<strong>Response Data:</strong>
<strong>{{ _("Response Data:") }}:</strong>
<pre class="bg-light p-2 rounded"><code>{{ log.response_data|pprint }}</code></pre>
</div>
{% endif %}
{% if log.error_message %}
<div class="mb-3">
<strong>Error Message:</strong>
<strong>{{ _("Error Message:") }}:</strong>
<div class="alert alert-danger">{{ log.error_message }}</div>
</div>
{% endif %}
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
</div>
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{{ _("Close") }}</button>
</div>
</div>
</div>