Compare commits

..
Author SHA1 Message Date
Muhsin e496e0712d fix: specs 2025-09-25 20:32:07 +05:30
Muhsin 654c951998 fix: inconsistencies for twilio whatsapp mobile numbers 2025-09-25 19:20:14 +05:30
34 changed files with 755 additions and 510 deletions
@@ -17,8 +17,8 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController
before_action :set_include_contact_inboxes, only: [:index, :active, :search, :filter, :show, :update]
def index
@contacts_count = resolved_contacts.count
@contacts = fetch_contacts(resolved_contacts)
@contacts_count = @contacts.total_count
end
def search
@@ -29,8 +29,8 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController
OR contacts.additional_attributes->>\'company_name\' ILIKE :search',
search: "%#{params[:q].strip}%"
)
@contacts_count = contacts.count
@contacts = fetch_contacts(contacts)
@contacts_count = @contacts.total_count
end
def import
@@ -55,8 +55,8 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController
def active
contacts = Current.account.contacts.where(id: ::OnlineStatusTracker
.get_available_contact_ids(Current.account.id))
@contacts_count = contacts.count
@contacts = fetch_contacts(contacts)
@contacts_count = @contacts.total_count
end
def show; end
@@ -133,14 +133,13 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController
end
def fetch_contacts(contacts)
# Build includes hash to avoid separate query when contact_inboxes are needed
includes_hash = { avatar_attachment: [:blob] }
includes_hash[:contact_inboxes] = { inbox: :channel } if @include_contact_inboxes
contacts_with_avatar = filtrate(contacts)
.includes([{ avatar_attachment: [:blob] }])
.page(@current_page).per(RESULTS_PER_PAGE)
filtrate(contacts)
.includes(includes_hash)
.page(@current_page)
.per(RESULTS_PER_PAGE)
return contacts_with_avatar.includes([{ contact_inboxes: [:inbox] }]) if @include_contact_inboxes
contacts_with_avatar
end
def build_contact_inbox
@@ -68,17 +68,13 @@ export const registerSubscription = (onSuccess = () => {}) => {
.then(() => {
onSuccess();
})
.catch(error => {
// eslint-disable-next-line no-console
console.error('Push subscription registration failed:', error);
.catch(() => {
useAlert('This browser does not support desktop notification');
});
};
export const requestPushPermissions = ({ onSuccess }) => {
if (!('Notification' in window)) {
// eslint-disable-next-line no-console
console.warn('Notification is not supported');
useAlert('This browser does not support desktop notification');
} else if (Notification.permission === 'granted') {
registerSubscription(onSuccess);
@@ -24,7 +24,7 @@
"CREATE_NEW_ACCOUNT": "Create a new account",
"SUBMIT": "Login",
"SAML": {
"LABEL": "Login via SSO",
"LABEL": "Log in via SSO",
"TITLE": "Initiate Single Sign-on (SSO)",
"SUBTITLE": "Enter your work email to access your organization",
"BACK_TO_LOGIN": "Login via Password",
@@ -27,20 +27,15 @@
"LABEL": "Password",
"PLACEHOLDER": "Password",
"ERROR": "Password is too short.",
"IS_INVALID_PASSWORD": "Password should contain atleast 1 uppercase letter, 1 lowercase letter, 1 number and 1 special character.",
"REQUIREMENTS_LENGTH": "At least 6 characters long",
"REQUIREMENTS_UPPERCASE": "At least one uppercase letter",
"REQUIREMENTS_LOWERCASE": "At least one lowercase letter",
"REQUIREMENTS_NUMBER": "At least one number",
"REQUIREMENTS_SPECIAL": "At least one special character"
"IS_INVALID_PASSWORD": "Password should contain atleast 1 uppercase letter, 1 lowercase letter, 1 number and 1 special character."
},
"CONFIRM_PASSWORD": {
"LABEL": "Confirm password",
"PLACEHOLDER": "Confirm password",
"ERROR": "Passwords do not match."
"ERROR": "Password doesnot match."
},
"API": {
"SUCCESS_MESSAGE": "Registration Successful",
"SUCCESS_MESSAGE": "Registration Successfull",
"ERROR_MESSAGE": "Could not connect to Woot server. Please try again."
},
"SUBMIT": "Create account",
@@ -139,7 +139,7 @@
},
"WHATSAPP": {
"HEADER_TITLE": "WhatsApp campaigns",
"NEW_CAMPAIGN": "Kampanya Oluştur",
"NEW_CAMPAIGN": "Create campaign",
"EMPTY_STATE": {
"TITLE": "No WhatsApp campaigns are available",
"SUBTITLE": "Launch a WhatsApp campaign to reach your customers directly. Send offers or make announcements with ease. Click 'Create campaign' to get started."
@@ -171,8 +171,8 @@
},
"TEMPLATE": {
"LABEL": "WhatsApp Template",
"PLACEHOLDER": "Şablon seçin",
"INFO": "Bu kampanya için kullanılacak şablonu seçin.",
"PLACEHOLDER": "Select a template",
"INFO": "Select a template to use for this campaign.",
"ERROR": "Template is required",
"PREVIEW_TITLE": "{templateName} işleniyor",
"LANGUAGE": "Dil",
@@ -51,6 +51,6 @@
"PLACEHOLDER": "Süre girin"
},
"CHANNEL_SELECTOR": {
"COMING_SOON": "Çok yakında!"
"COMING_SOON": "Coming Soon!"
}
}
@@ -18,9 +18,9 @@
"CREATED_AT_LABEL": "Oluşturuldu",
"NEW_MESSAGE": "Yeni Mesaj",
"CALL": "Ara",
"CALL_UNDER_DEVELOPMENT": "Arama özelliği geliştirme aşamasındadır",
"CALL_UNDER_DEVELOPMENT": "Calling is under development",
"VOICE_INBOX_PICKER": {
"TITLE": "Sesli gelen kutusu seçin"
"TITLE": "Choose a voice inbox"
},
"CONVERSATIONS": {
"NO_RECORDS_FOUND": "Bu kişiyle ilişkilendirilmiş önceki görüşme yok.",
@@ -554,12 +554,12 @@
"WROTE": "wrote",
"YOU": "Sen",
"SAVE": "Save note",
"ADD_NOTE": "Kişi notu ekle",
"ADD_NOTE": "Add contact note",
"EXPAND": "Genişlet",
"COLLAPSE": "Daralt",
"NO_NOTES": "Not yok, kişi detayları sayfasından not ekleyebilirsiniz.",
"EMPTY_STATE": "There are no notes associated to this contact. You can add a note by typing in the box above.",
"CONVERSATION_EMPTY_STATE": "Henüz not yok. Not eklemek için Not ekle düğmesini kullanın."
"CONVERSATION_EMPTY_STATE": "There are no notes yet. Use the Add note button to create one."
}
},
"EMPTY_STATE": {
@@ -3,30 +3,30 @@
"MODAL": {
"TITLE": "Twilio Templates",
"SUBTITLE": "Select the Twilio template you want to send",
"TEMPLATE_SELECTED_SUBTITLE": "Şablonu yapılandır: {templateName}"
"TEMPLATE_SELECTED_SUBTITLE": "Configure template: {templateName}"
},
"PICKER": {
"SEARCH_PLACEHOLDER": "Şablon Ara",
"NO_TEMPLATES_FOUND": "İçin hiç şablon bulunamadı",
"NO_CONTENT": "No content",
"HEADER": "Başlık",
"BODY": "Gövde",
"FOOTER": "Altbilgi",
"BUTTONS": "Butonlar",
"HEADER": "Header",
"BODY": "Body",
"FOOTER": "Footer",
"BUTTONS": "Buttons",
"CATEGORY": "Kategori",
"MEDIA_CONTENT": "Medya İçeriği",
"MEDIA_CONTENT_FALLBACK": "medya içeriği",
"MEDIA_CONTENT": "Media Content",
"MEDIA_CONTENT_FALLBACK": "media content",
"NO_TEMPLATES_AVAILABLE": "No Twilio templates available. Click refresh to sync templates from Twilio.",
"REFRESH_BUTTON": "Şablonları yenile",
"REFRESH_SUCCESS": "Şablonların yenilenmesi başlatıldı. Güncelleme birkaç dakika sürebilir.",
"REFRESH_ERROR": "Şablonları yenileme işlemi başarısız oldu. Lütfen tekrar deneyin.",
"REFRESH_BUTTON": "Refresh templates",
"REFRESH_SUCCESS": "Templates refresh initiated. It may take a couple of minutes to update.",
"REFRESH_ERROR": "Failed to refresh templates. Please try again.",
"LABELS": {
"LANGUAGE": "Dil",
"TEMPLATE_BODY": "Şablon İçeriği",
"CATEGORY": "Kategori"
},
"TYPES": {
"MEDIA": "Medya",
"MEDIA": "Media",
"QUICK_REPLY": "Quick Reply",
"TEXT": "Metin"
}
@@ -39,7 +39,7 @@
"GO_BACK_LABEL": "Geri Git",
"SEND_MESSAGE_LABEL": "Mesaj Gönder",
"FORM_ERROR_MESSAGE": "Lütfen göndermeden önce tüm değişkenleri doldurun",
"MEDIA_HEADER_LABEL": "{type} Başlık",
"MEDIA_HEADER_LABEL": "{type} Header",
"MEDIA_URL_LABEL": "Enter full media URL",
"MEDIA_URL_PLACEHOLDER": "https://example.com/image.jpg"
},
@@ -35,11 +35,11 @@
"API_HOURS_WINDOW": "Bu sohbete yalnızca {hours} saat içinde yanıt verebilirsiniz",
"NOT_ASSIGNED_TO_YOU": "Bu görüşme size atanmamış. Bu konuşmayı kendinize atamak ister misiniz?",
"ASSIGN_TO_ME": "Bana ata",
"BOT_HANDOFF_MESSAGE": "Şu anda bir asistan veya bot tarafından yürütülen bir konuşmaya yanıt veriyorsunuz.",
"BOT_HANDOFF_ACTION": "Size atayın ve açık olarak işaretleyin",
"BOT_HANDOFF_REOPEN_ACTION": "Konuşmayı açık olarak işaretle",
"BOT_HANDOFF_SUCCESS": "Konuşma size devredildi",
"BOT_HANDOFF_ERROR": "Konuşma devralınamadı. Lütfen tekrar deneyin.",
"BOT_HANDOFF_MESSAGE": "You are responding to a conversation which is currently handled by an assistant or a bot.",
"BOT_HANDOFF_ACTION": "Mark open and assign to you",
"BOT_HANDOFF_REOPEN_ACTION": "Mark conversation open",
"BOT_HANDOFF_SUCCESS": "Conversation has been handed over to you",
"BOT_HANDOFF_ERROR": "Failed to take over the conversation. Please try again.",
"TWILIO_WHATSAPP_CAN_REPLY": "Bu konuşmaya yalnızca şablon mesaj kullanarak yanıt verebilirsiniz, çünkü",
"TWILIO_WHATSAPP_24_HOURS_WINDOW": "24 saat mesaj penceresi kısıtlaması",
"OLD_INSTAGRAM_INBOX_REPLY_BANNER": "Bu Instagram hesabı yeni Instagram kanal gelen kutusuna taşındı. Tüm yeni mesajlar orada görünecek. Bu sohbetten artık mesaj gönderemezsiniz.",
@@ -72,15 +72,15 @@
"HIDE_LABELS": "Etiketleri Gizle"
},
"VOICE_CALL": {
"INCOMING_CALL": "Gelen arama",
"OUTGOING_CALL": "Giden arama",
"CALL_IN_PROGRESS": "Arama devam ediyor",
"NO_ANSWER": "Yanıt yok",
"MISSED_CALL": "Cevapsız arama",
"CALL_ENDED": "Arama sona erdi",
"NOT_ANSWERED_YET": "Henüz yanıtlanmadı",
"THEY_ANSWERED": "Onlar yanıtladı",
"YOU_ANSWERED": "Siz yanıtladınız"
"INCOMING_CALL": "Incoming call",
"OUTGOING_CALL": "Outgoing call",
"CALL_IN_PROGRESS": "Call in progress",
"NO_ANSWER": "No answer",
"MISSED_CALL": "Missed call",
"CALL_ENDED": "Call ended",
"NOT_ANSWERED_YET": "Not answered yet",
"THEY_ANSWERED": "They answered",
"YOU_ANSWERED": "You answered"
},
"HEADER": {
"RESOLVE_ACTION": "Çözüldü",
@@ -160,9 +160,9 @@
"AGENTS_LOADING": "Temsilciler Yükleniyor...",
"ASSIGN_TEAM": "Takım ata",
"DELETE": "Sohbeti sil",
"OPEN_IN_NEW_TAB": "Yeni sekmede aç",
"COPY_LINK": "Konuşma bağlantısını kopyala",
"COPY_LINK_SUCCESS": "Konuşma bağlantısı panoya kopyalandı",
"OPEN_IN_NEW_TAB": "Open in new tab",
"COPY_LINK": "Copy conversation link",
"COPY_LINK_SUCCESS": "Conversation link copied to clipboard",
"API": {
"AGENT_ASSIGNMENT": {
"SUCCESFUL": "Sohbet kimliği {conversationId} \"{agentName}\" tarafından atanmış",
@@ -7,6 +7,6 @@
},
"CLOSE": "Kapat",
"BETA": "Beta",
"BETA_DESCRIPTION": "Bu özellik beta aşamasındadır ve geliştirdikçe değişiklik gösterebilir."
"BETA_DESCRIPTION": "This feature is in beta and may change as we improve it."
}
}
@@ -3,7 +3,7 @@
"LIMIT_MESSAGES": {
"CONVERSATION": "Sohbet sınırını aştınız. Hacker planı yalnızca 500 sohbete izin verir.",
"INBOXES": "Gelen kutusu sınırını aştınız. Hacker planı yalnızca web sitesi canlı sohbetini destekler. E-posta, WhatsApp gibi ek gelen kutuları ücretli plan gerektirir.",
"AGENTS": "Temsilci sınırını aştınız. Planınız yalnızca {allowedAgents} temsilciye izin veriyor.",
"AGENTS": "You have exceeded the agent limit. Your plan only allows {allowedAgents} agents.",
"NON_ADMIN": "Tüm özellikleri kullanmaya devam etmek için lütfen yöneticinizle iletişime geçin ve planı yükseltin."
},
"TITLE": "Hesap Ayarları",
@@ -732,7 +732,7 @@
"HOME_PAGE_LINK": {
"LABEL": "Home page link",
"PLACEHOLDER": "Portal ana sayfa bağlantısı",
"ERROR": "Geçerli bir URL girin. Ana sayfa bağlantısı http:// veya https:// ile başlamalıdır."
"ERROR": "Enter a valid URL. The Home page link must start with 'http://' or 'https://'."
},
"SLUG": {
"LABEL": "Slug",
@@ -754,14 +754,14 @@
"HEADER": "Özel domain",
"LABEL": "Özel domain:",
"DESCRIPTION": "You can host your portal on a custom domain. For instance, if your website is yourdomain.com and you want your portal available at docs.yourdomain.com, simply enter that in this field.",
"STATUS_DESCRIPTION": "Özel portalınız, doğrulandıktan sonra hemen çalışmaya başlayacaktır.",
"STATUS_DESCRIPTION": "Your custom portal will start working as soon as it is verified.",
"PLACEHOLDER": "Portal özel domain",
"EDIT_BUTTON": "Düzenle",
"ADD_BUTTON": "Add custom domain",
"STATUS": {
"LIVE": "Canlı",
"PENDING": "Doğrulama bekleniyor",
"ERROR": "Doğrulama başarısız"
"PENDING": "Awaiting verification",
"ERROR": "Verification failed"
},
"DIALOG": {
"ADD_HEADER": "Add custom domain",
@@ -771,14 +771,14 @@
"LABEL": "Özel domain",
"PLACEHOLDER": "Portal özel domain",
"ERROR": "Custom domain is required",
"FORMAT_ERROR": "Lütfen geçerli bir alan adı URL'si girin, örneğin docs.yourdomain.com"
"FORMAT_ERROR": "Please enter a valid domain URL e.g. docs.yourdomain.com"
},
"DNS_CONFIGURATION_DIALOG": {
"HEADER": "DNS configuration",
"DESCRIPTION": "Log in to the account you have with your DNS provider, and add a CNAME record for subdomain pointing to chatwoot.help",
"COPY": "CNAME başarıyla kopyalandı",
"COPY": "Successfully copied CNAME",
"SEND_INSTRUCTIONS": {
"HEADER": "Talimatları gönder",
"HEADER": "Send instructions",
"DESCRIPTION": "Bu adımı geliştirme ekibinizden birinin yapmasını tercih ediyorsanız, aşağıya mail adresini girin; gerekli talimatları onlara gönderelim.",
"PLACEHOLDER": "E-posta adreslerini girin",
"ERROR": "Geçerli bir mail adresi girin",
@@ -810,53 +810,53 @@
}
},
"PDF_UPLOAD": {
"TITLE": "PDF Belgesini Yükle",
"DESCRIPTION": "AI kullanarak otomatik olarak SSS'leri oluşturmak için bir PDF belgesi yükleyin",
"DRAG_DROP_TEXT": "PDF dosyanızı buraya sürükleyip bırakın veya tıklayarak seçin",
"SELECT_FILE": "PDF Dosyasını Seç",
"ADDITIONAL_CONTEXT_LABEL": "Ek Bağlam (İsteğe Bağlı)",
"TITLE": "Upload PDF Document",
"DESCRIPTION": "Upload a PDF document to automatically generate FAQs using AI",
"DRAG_DROP_TEXT": "Drag and drop your PDF file here, or click to select",
"SELECT_FILE": "Select PDF File",
"ADDITIONAL_CONTEXT_LABEL": "Additional Context (Optional)",
"ADDITIONAL_CONTEXT_PLACEHOLDER": "Provide any additional context or instructions for FAQ generation...",
"UPLOADING": "Yükleniyor ...",
"UPLOAD": "Yükle & İşle",
"UPLOAD": "Upload & Process",
"CANCEL": "İptal Et",
"ERROR_INVALID_TYPE": "Lütfen geçerli bir PDF dosyası seçin",
"ERROR_FILE_TOO_LARGE": "Dosya boyutu 512 MB'tan az olmalıdır",
"ERROR_UPLOAD_FAILED": "PDF yüklenemedi. Lütfen tekrar deneyin."
"ERROR_INVALID_TYPE": "Please select a valid PDF file",
"ERROR_FILE_TOO_LARGE": "File size must be less than 512MB",
"ERROR_UPLOAD_FAILED": "Failed to upload PDF. Please try again."
},
"PDF_DOCUMENTS": {
"TITLE": "PDF Belgeleri",
"DESCRIPTION": "Yüklenen PDF belgelerini yönetin ve bunlardan SSS'ler oluşturun",
"UPLOAD_PDF": "PDF yükle",
"UPLOAD_FIRST_PDF": "İlk PDF'inizi yükleyin",
"UPLOADED_BY": "Yükleyen",
"TITLE": "PDF Documents",
"DESCRIPTION": "Manage uploaded PDF documents and generate FAQs from them",
"UPLOAD_PDF": "Upload PDF",
"UPLOAD_FIRST_PDF": "Upload your first PDF",
"UPLOADED_BY": "Uploaded by",
"GENERATE_FAQS": "Generate FAQs",
"GENERATING": "Oluşturuluyor...",
"CONFIRM_DELETE": "{filename} silmek istediğinizden emin misiniz?",
"EMPTY_STATE": {
"TITLE": "Henüz PDF belgesi yok",
"TITLE": "No PDF documents yet",
"DESCRIPTION": "Upload PDF documents to automatically generate FAQs using AI"
},
"STATUS": {
"UPLOADED": "Hazır",
"PROCESSING": "İşleniyor",
"UPLOADED": "Ready",
"PROCESSING": "Processing",
"PROCESSED": "Tamamlandı",
"FAILED": "Başarısız"
"FAILED": "Failed"
}
},
"CONTENT_GENERATION": {
"TITLE": "Content Generation",
"DESCRIPTION": "Upload PDF documents to automatically generate FAQ content using AI",
"UPLOAD_TITLE": "PDF Belgesini Yükle",
"DRAG_DROP": "PDF dosyanızı buraya sürükleyip bırakın veya tıklayarak seçin",
"SELECT_FILE": "PDF Dosyasını Seç",
"UPLOADING": "Belge işleniyor...",
"UPLOAD_TITLE": "Upload PDF Document",
"DRAG_DROP": "Drag and drop your PDF file here, or click to select",
"SELECT_FILE": "Select PDF File",
"UPLOADING": "Processing document...",
"UPLOAD_SUCCESS": "Document processed successfully!",
"UPLOAD_ERROR": "Failed to upload document. Please try again.",
"INVALID_FILE_TYPE": "Lütfen geçerli bir PDF dosyası seçin",
"FILE_TOO_LARGE": "Dosya boyutu 512 MB'tan az olmalıdır",
"INVALID_FILE_TYPE": "Please select a valid PDF file",
"FILE_TOO_LARGE": "File size must be less than 512MB",
"GENERATED_CONTENT": "Generated FAQ Content",
"PUBLISH_SELECTED": "Publish Selected",
"PUBLISHING": "Yayınlanıyor...",
"PUBLISHING": "Publishing...",
"FROM_DOCUMENT": "From document",
"NO_CONTENT": "No generated content available. Upload a PDF document to get started.",
"LOADING": "Loading generated content..."
@@ -74,14 +74,14 @@
"DELETE_ALL_READ": "Tüm Okunmuş Bildirimler Silindi"
},
"REAUTHORIZE": {
"TITLE": "Yeniden yetkilendirme gerekli",
"DESCRIPTION": "WhatsApp bağlantınızın süresi doldu. Mesaj almaya ve göndermeye devam etmek için lütfen yeniden bağlanın.",
"BUTTON_TEXT": "WhatsApp'ı yeniden bağla",
"LOADING_FACEBOOK": "Facebook SDK yükleniyor...",
"SUCCESS": "WhatsApp başarıyla yeniden bağlandı",
"ERROR": "WhatsApp'a yeniden bağlanılamadı. Lütfen tekrar deneyin.",
"WHATSAPP_APP_ID_MISSING": "WhatsApp Uygulama Kimliği yapılandırılmamış. Lütfen yöneticinize başvurun.",
"WHATSAPP_CONFIG_ID_MISSING": "WhatsApp Yapılandırma Kimliği yapılandırılmamış. Lütfen yöneticinize başvurun.",
"TITLE": "Reauthorization Required",
"DESCRIPTION": "Your WhatsApp connection has expired. Please reconnect to continue receiving and sending messages.",
"BUTTON_TEXT": "Reconnect WhatsApp",
"LOADING_FACEBOOK": "Loading Facebook SDK...",
"SUCCESS": "WhatsApp reconnected successfully",
"ERROR": "Failed to reconnect WhatsApp. Please try again.",
"WHATSAPP_APP_ID_MISSING": "WhatsApp App ID is not configured. Please contact your administrator.",
"WHATSAPP_CONFIG_ID_MISSING": "WhatsApp Configuration ID is not configured. Please contact your administrator.",
"CONFIGURATION_ERROR": "Configuration error occurred during reauthorization.",
"FACEBOOK_LOAD_ERROR": "Failed to load Facebook SDK. Please try again.",
"TROUBLESHOOTING": {
@@ -225,13 +225,13 @@
"WHATSAPP_EMBEDDED": "WhatsApp Business",
"TWILIO": "Twilio",
"WHATSAPP_CLOUD": "WhatsApp Bulut",
"WHATSAPP_CLOUD_DESC": "Meta üzerinden hızlı kurulum",
"TWILIO_DESC": "Twilio kimlik bilgileriyle bağlanın",
"WHATSAPP_CLOUD_DESC": "Quick setup through Meta",
"TWILIO_DESC": "Connect via Twilio credentials",
"360_DIALOG": "360Dialog"
},
"SELECT_PROVIDER": {
"TITLE": "API sağlayıcınızı seçin",
"DESCRIPTION": "WhatsApp sağlayıcınızı seçin. Kurulum gerektirmeyen Meta üzerinden doğrudan bağlanabilir veya hesap bilgilerinizi kullanarak Twilio üzerinden bağlanabilirsiniz."
"TITLE": "Select your API provider",
"DESCRIPTION": "Choose your WhatsApp provider. You can connect directly through Meta which requires no setup, or connect through Twilio using your account credentials."
},
"INBOX_NAME": {
"LABEL": "Gelen Kutusu Adı",
@@ -272,69 +272,69 @@
},
"SUBMIT_BUTTON": "WhatsApp Kanalı Oluştur",
"EMBEDDED_SIGNUP": {
"TITLE": "Meta ile hızlı kurulum",
"DESC": "Yeni numaraları hızlı bir şekilde bağlamak için WhatsApp Embedded Signup akışını kullanın. WhatsApp Business hesabınıza giriş yapmak için Meta'ya yönlendirileceksiniz. Yönetici erişimi, kurulumu sorunsuz ve kolay hale getirmeye yardımcı olacaktır.",
"TITLE": "Quick setup with Meta",
"DESC": "Use the WhatsApp Embedded Signup flow to quickly connect new numbers. You will be redirected to Meta to log into your WhatsApp Business account. Having admin access will help make the setup smooth and easy.",
"BENEFITS": {
"TITLE": "Gömülü Kaydın Faydaları:",
"EASY_SETUP": "Manuel yapılandırma gerekmez",
"SECURE_AUTH": "Güvenli OAuth tabanlı kimlik doğrulama",
"AUTO_CONFIG": "Otomatik webhook ve telefon numarası yapılandırması"
"TITLE": "Benefits of Embedded Signup:",
"EASY_SETUP": "No manual configuration required",
"SECURE_AUTH": "Secure OAuth based authentication",
"AUTO_CONFIG": "Automatic webhook and phone number configuration"
},
"LEARN_MORE": {
"TEXT": "Entegre kayıt, fiyatlandırma ve sınırlamalar hakkında daha fazla bilgi için {link} adresini ziyaret edin.",
"LINK_TEXT": "bu bağlantı"
"TEXT": "To learn more about integrated signup, pricing, and limitations, visit {link}.",
"LINK_TEXT": "this link"
},
"SUBMIT_BUTTON": "WhatsApp Business ile bağlantı kurun",
"AUTH_PROCESSING": "Meta ile kimlik doğrulama",
"WAITING_FOR_BUSINESS_INFO": "Lütfen Meta penceresinde işletme kurulumunu tamamlayın...",
"PROCESSING": "WhatsApp Business Hesabınızı kurma",
"LOADING_SDK": "Facebook SDK yükleniyor...",
"CANCELLED": "WhatsApp Kaydı iptal edildi",
"SUCCESS_TITLE": "WhatsApp Business Hesabı Bağlandı!",
"WAITING_FOR_AUTH": "Kimlik doğrulaması bekleniyor...",
"INVALID_BUSINESS_DATA": "Facebooktan geçersiz işletme verisi alındı. Lütfen tekrar deneyin.",
"SIGNUP_ERROR": "Kayıt hatası oluştu",
"AUTH_NOT_COMPLETED": "Kimlik doğrulama tamamlanmadı. Lütfen işlemi yeniden başlatın.",
"SUCCESS_FALLBACK": "WhatsApp Business Hesabı başarıyla yapılandırıldı",
"MANUAL_FALLBACK": "Numaranız zaten WhatsApp Business Platformuna (API) bağlıysa veya kendi numaranızı ekleyen bir teknoloji sağlayıcısıysanız, lütfen {link} akışını kullanın",
"MANUAL_LINK_TEXT": "manuel kurulum akışı"
"SUBMIT_BUTTON": "Connect with WhatsApp Business",
"AUTH_PROCESSING": "Authenticating with Meta",
"WAITING_FOR_BUSINESS_INFO": "Please complete business setup in the Meta window...",
"PROCESSING": "Setting up your WhatsApp Business Account",
"LOADING_SDK": "Loading Facebook SDK...",
"CANCELLED": "WhatsApp Signup was cancelled",
"SUCCESS_TITLE": "WhatsApp Business Account Connected!",
"WAITING_FOR_AUTH": "Waiting for authentication...",
"INVALID_BUSINESS_DATA": "Invalid business data received from Facebook. Please try again.",
"SIGNUP_ERROR": "Signup error occurred",
"AUTH_NOT_COMPLETED": "Authentication not completed. Please restart the process.",
"SUCCESS_FALLBACK": "WhatsApp Business Account has been successfully configured",
"MANUAL_FALLBACK": "If your number is already connected to the WhatsApp Business Platform (API), or if youre a tech provider onboarding your own number, please use the {link} flow",
"MANUAL_LINK_TEXT": "manual setup flow"
},
"API": {
"ERROR_MESSAGE": "WhatsApp kanalını kaydedemedik"
}
},
"VOICE": {
"TITLE": "Ses Kanalı",
"DESC": "Twilio Voiceu entegre edin ve müşterilerinize telefon aramalarıyla destek vermeye başlayın.",
"TITLE": "Voice Channel",
"DESC": "Integrate Twilio Voice and start supporting your customers via phone calls.",
"PHONE_NUMBER": {
"LABEL": "Telefon Numarası",
"PLACEHOLDER": "Telefon numaranızı girin (örn. +901234567890)",
"ERROR": "Lütfen E.164 formatında geçerli bir telefon numarası girin (örn. +901234567890)"
"PLACEHOLDER": "Enter your phone number (e.g. +1234567890)",
"ERROR": "Please provide a valid phone number in E.164 format (e.g. +1234567890)"
},
"TWILIO": {
"ACCOUNT_SID": {
"LABEL": "Hesap SID'si",
"PLACEHOLDER": "Twilio Account SIDinizi girin",
"REQUIRED": "Account SID gereklidir"
"PLACEHOLDER": "Enter your Twilio Account SID",
"REQUIRED": "Account SID is required"
},
"AUTH_TOKEN": {
"LABEL": "Yetkilendirme Jetonu",
"PLACEHOLDER": "Twilio Auth Tokeninizi girin",
"REQUIRED": "Auth Token gereklidir"
"PLACEHOLDER": "Enter your Twilio Auth Token",
"REQUIRED": "Auth Token is required"
},
"API_KEY_SID": {
"LABEL": "API Anahtar SID",
"PLACEHOLDER": "Twilio API Key SIDinizi girin",
"REQUIRED": "API Key SID gereklidir"
"PLACEHOLDER": "Enter your Twilio API Key SID",
"REQUIRED": "API Key SID is required"
},
"API_KEY_SECRET": {
"LABEL": "API Anahtar Sırrı",
"PLACEHOLDER": "Twilio API Key Secret’ınızı girin",
"REQUIRED": "API Key Secret gereklidir"
"PLACEHOLDER": "Enter your Twilio API Key Secret",
"REQUIRED": "API Key Secret is required"
}
},
"CONFIGURATION": {
"TWILIO_VOICE_URL_TITLE": "Twilio Ses URL",
"TWILIO_VOICE_URL_TITLE": "Twilio Voice URL",
"TWILIO_VOICE_URL_SUBTITLE": "Configure this URL as the Voice URL on your Twilio phone number and TwiML App.",
"TWILIO_STATUS_URL_TITLE": "Twilio Status Callback URL",
"TWILIO_STATUS_URL_SUBTITLE": "Configure this URL as the Status Callback URL on your Twilio phone number."
@@ -426,12 +426,12 @@
"AUTH": {
"TITLE": "Bir Kanal Seçin",
"DESC": "Chatwoot, canlı sohbet widget'ları, Facebook Messenger, Twitter profilleri, WhatsApp, E-postalar vb. olarak kanalları destekler. Özel bir kanal oluşturmak istiyorsanız, API kanalını kullanarak bunu oluşturabilirsiniz. Başlamak için aşağıdaki kanallardan birini seçin.",
"TITLE_NEXT": "Kurulumu tamamlayın",
"TITLE_NEXT": "Complete the setup",
"TITLE_FINISH": "Voilà!",
"CHANNEL": {
"WEBSITE": {
"TITLE": "Website",
"DESCRIPTION": "Canlı sohbet widget'ı oluşturun"
"DESCRIPTION": "Create a live-chat widget"
},
"FACEBOOK": {
"TITLE": "Facebook\n",
@@ -466,7 +466,7 @@
"DESCRIPTION": "Connect your instagram account"
},
"VOICE": {
"TITLE": "Ses",
"TITLE": "Voice",
"DESCRIPTION": "Integrate with Twilio Voice"
}
}
@@ -619,7 +619,7 @@
"MESSENGER_HEADING": "Messenger Komut Dosyası",
"MESSENGER_SUB_HEAD": "Bu düğmeyi gövde etiketinizin içine yerleştirin",
"ALLOWED_DOMAINS": {
"TITLE": "İzin Verilen Alan Adları",
"TITLE": "Allowed Domains",
"SUBTITLE": "Add wildcard or regular domains separated by commas (leave blank to allow all), e.g. *.chatwoot.dev, chatwoot.com.",
"PLACEHOLDER": "Enter domains separated by commas (eg: *.chatwoot.dev, chatwoot.com)"
},
@@ -660,7 +660,7 @@
"WHATSAPP_EMBEDDED_SIGNUP_TITLE": "WhatsApp Embedded Signup",
"WHATSAPP_EMBEDDED_SIGNUP_SUBHEADER": "This inbox is connected through WhatsApp embedded signup.",
"WHATSAPP_EMBEDDED_SIGNUP_DESCRIPTION": "You can reconfigure this inbox to update your WhatsApp Business settings.",
"WHATSAPP_RECONFIGURE_BUTTON": "Yeniden yapılandır",
"WHATSAPP_RECONFIGURE_BUTTON": "Reconfigure",
"WHATSAPP_CONNECT_TITLE": "Connect to WhatsApp Business",
"WHATSAPP_CONNECT_SUBHEADER": "Upgrade to WhatsApp embedded signup for easier management.",
"WHATSAPP_CONNECT_DESCRIPTION": "Connect this inbox to WhatsApp Business for enhanced features and easier management.",
@@ -669,14 +669,14 @@
"WHATSAPP_CONNECT_ERROR": "Failed to connect to WhatsApp Business. Please try again.",
"WHATSAPP_RECONFIGURE_SUCCESS": "Successfully reconfigured WhatsApp Business!",
"WHATSAPP_RECONFIGURE_ERROR": "Failed to reconfigure WhatsApp Business. Please try again.",
"WHATSAPP_APP_ID_MISSING": "WhatsApp Uygulama Kimliği yapılandırılmamış. Lütfen yöneticinize başvurun.",
"WHATSAPP_CONFIG_ID_MISSING": "WhatsApp Yapılandırma Kimliği yapılandırılmamış. Lütfen yöneticinize başvurun.",
"WHATSAPP_APP_ID_MISSING": "WhatsApp App ID is not configured. Please contact your administrator.",
"WHATSAPP_CONFIG_ID_MISSING": "WhatsApp Configuration ID is not configured. Please contact your administrator.",
"WHATSAPP_LOGIN_CANCELLED": "WhatsApp login was cancelled. Please try again.",
"WHATSAPP_WEBHOOK_TITLE": "Webhook Onay Anahtarı",
"WHATSAPP_WEBHOOK_SUBHEADER": "Bu belirteç, webhook uç noktasının gerçekliğini doğrulamak için kullanılır.",
"WHATSAPP_TEMPLATES_SYNC_TITLE": "Şablonları Senkronize Et",
"WHATSAPP_TEMPLATES_SYNC_SUBHEADER": "WhatsApp'tan mesaj şablonlarını manuel olarak senkronize ederek mevcut şablonlarınızı güncelleyin.",
"WHATSAPP_TEMPLATES_SYNC_BUTTON": "Şablonları Senkronize Et",
"WHATSAPP_TEMPLATES_SYNC_TITLE": "Sync Templates",
"WHATSAPP_TEMPLATES_SYNC_SUBHEADER": "Manually sync message templates from WhatsApp to update your available templates.",
"WHATSAPP_TEMPLATES_SYNC_BUTTON": "Sync Templates",
"WHATSAPP_TEMPLATES_SYNC_SUCCESS": "Templates sync initiated successfully. It may take a couple of minutes to update.",
"UPDATE_PRE_CHAT_FORM_SETTINGS": "Sohbet Öncesi Form Ayarlarını Güncelleme"
},
@@ -946,7 +946,7 @@
"LINE": "Line",
"API": "API Kanalı",
"INSTAGRAM": "Instagram",
"VOICE": "Ses"
"VOICE": "Voice"
}
}
}
@@ -487,12 +487,12 @@
"ASSISTANT": "Assistant"
},
"BASIC_SETTINGS": {
"TITLE": "Temel ayarlar",
"DESCRIPTION": "Konuşmayı sonlandırırken veya bir operatöre aktarırken asistanın söyleyeceği sözleri özelleştirin."
"TITLE": "Basic settings",
"DESCRIPTION": "Customize what the assistant says when ending a conversation or transferring to a human."
},
"SYSTEM_SETTINGS": {
"TITLE": "Sistem ayarları",
"DESCRIPTION": "Konuşmayı sonlandırırken veya bir operatöre aktarırken asistanın söyleyeceği sözleri özelleştirin."
"TITLE": "System settings",
"DESCRIPTION": "Customize what the assistant says when ending a conversation or transferring to a human."
},
"CONTROL_ITEMS": {
"TITLE": "The Fun Stuff",
@@ -534,16 +534,16 @@
},
"BULK_ACTION": {
"SELECTED": "{count} item selected | {count} items selected",
"SELECT_ALL": "Tümünü seç ({count})",
"UNSELECT_ALL": "Tümünü kaldır ({count})",
"SELECT_ALL": "Select all ({count})",
"UNSELECT_ALL": "Unselect all ({count})",
"BULK_DELETE_BUTTON": "Sil"
},
"ADD": {
"SUGGESTED": {
"TITLE": "Example guardrails",
"ADD": "Tümünü ekle",
"ADD_SINGLE": "Bunu ekle",
"SAVE": "Ekle ve kaydet (↵)",
"ADD": "Add all",
"ADD_SINGLE": "Add this",
"SAVE": "Add and save (↵)",
"PLACEHOLDER": "Type in another guardrail..."
},
"NEW": {
@@ -551,11 +551,11 @@
"CREATE": "Yarat",
"CANCEL": "İptal Et",
"PLACEHOLDER": "Type in another guardrail...",
"TEST_ALL": "Tümünü test et"
"TEST_ALL": "Test all"
}
},
"LIST": {
"SEARCH_PLACEHOLDER": "Ara..."
"SEARCH_PLACEHOLDER": "Search..."
},
"EMPTY_MESSAGE": "No guardrails found. Create or add examples to begin.",
"SEARCH_EMPTY_MESSAGE": "No guardrails found for this search.",
@@ -581,17 +581,17 @@
"TITLE": "Response Guidelines"
},
"BULK_ACTION": {
"SELECTED": "{count} eşya seçildi | {count} eşya seçildi",
"SELECT_ALL": "Tümünü seç ({count})",
"UNSELECT_ALL": "Tümünü kaldır ({count})",
"SELECTED": "{count} item selected | {count} items selected",
"SELECT_ALL": "Select all ({count})",
"UNSELECT_ALL": "Unselect all ({count})",
"BULK_DELETE_BUTTON": "Sil"
},
"ADD": {
"SUGGESTED": {
"TITLE": "Example response guidelines",
"ADD": "Tümünü ekle",
"ADD_SINGLE": "Bunu ekle",
"SAVE": "Ekle ve kaydet (↵)",
"ADD": "Add all",
"ADD_SINGLE": "Add this",
"SAVE": "Add and save (↵)",
"PLACEHOLDER": "Type in another response guideline..."
},
"NEW": {
@@ -599,11 +599,11 @@
"CREATE": "Yarat",
"CANCEL": "İptal Et",
"PLACEHOLDER": "Type in another response guideline...",
"TEST_ALL": "Tümünü test et"
"TEST_ALL": "Test all"
}
},
"LIST": {
"SEARCH_PLACEHOLDER": "Ara..."
"SEARCH_PLACEHOLDER": "Search..."
},
"EMPTY_MESSAGE": "No response guidelines found. Create or add examples to begin.",
"SEARCH_EMPTY_MESSAGE": "No response guidelines found for this search.",
@@ -630,15 +630,15 @@
},
"BULK_ACTION": {
"SELECTED": "{count} item selected | {count} items selected",
"SELECT_ALL": "Tümünü seç ({count})",
"UNSELECT_ALL": "Tümünü kaldır ({count})",
"SELECT_ALL": "Select all ({count})",
"UNSELECT_ALL": "Unselect all ({count})",
"BULK_DELETE_BUTTON": "Sil"
},
"ADD": {
"SUGGESTED": {
"TITLE": "Example scenarios",
"ADD": "Tümünü ekle",
"ADD_SINGLE": "Bunu ekle",
"ADD": "Add all",
"ADD_SINGLE": "Add this",
"TOOLS_USED": "Tools used :"
},
"NEW": {
@@ -667,10 +667,10 @@
},
"UPDATE": {
"CANCEL": "İptal Et",
"UPDATE": "Değişiklikleri güncelle"
"UPDATE": "Update changes"
},
"LIST": {
"SEARCH_PLACEHOLDER": "Ara..."
"SEARCH_PLACEHOLDER": "Search..."
},
"EMPTY_MESSAGE": "No scenarios found. Create or add examples to begin.",
"SEARCH_EMPTY_MESSAGE": "No scenarios found for this search.",
@@ -705,9 +705,9 @@
},
"FORM": {
"TYPE": {
"LABEL": "Belge Türü",
"LABEL": "Document Type",
"URL": "URL",
"PDF": "PDF Dosya"
"PDF": "PDF File"
},
"URL": {
"LABEL": "URL",
@@ -715,16 +715,16 @@
"ERROR": "Please provide a valid URL for the document"
},
"PDF_FILE": {
"LABEL": "PDF Dosya",
"CHOOSE_FILE": "PDF dosyasını seçin",
"ERROR": "Lütfen bir PDF dosyası seçin",
"HELP_TEXT": "Maksimum dosya boyutu: 10 MB",
"INVALID_TYPE": "Lütfen geçerli bir PDF dosyası seçin",
"TOO_LARGE": "Dosya boyutu 10 MB sınırını aşıyor"
"LABEL": "PDF File",
"CHOOSE_FILE": "Choose PDF file",
"ERROR": "Please select a PDF file",
"HELP_TEXT": "Maximum file size: 10MB",
"INVALID_TYPE": "Please select a valid PDF file",
"TOO_LARGE": "File size exceeds 10MB limit"
},
"NAME": {
"LABEL": "Belge Adı (İsteğe Bağlı)",
"PLACEHOLDER": "Belgeye bir ad girin"
"LABEL": "Document Name (Optional)",
"PLACEHOLDER": "Enter a name for the document"
},
"ASSISTANT": {
"LABEL": "Assistant",
@@ -759,9 +759,9 @@
"CONVERSATION": "Conversation #{id}"
},
"SELECTED": "{count} selected",
"SELECT_ALL": "Tümünü seç ({count})",
"UNSELECT_ALL": "Tümünü kaldır ({count})",
"SEARCH_PLACEHOLDER": "Sıkça Sorulan Soruları Ara...",
"SELECT_ALL": "Select all ({count})",
"UNSELECT_ALL": "Unselect all ({count})",
"SEARCH_PLACEHOLDER": "Search FAQs...",
"BULK_APPROVE_BUTTON": "Approve",
"BULK_DELETE_BUTTON": "Sil",
"BULK_APPROVE": {
@@ -24,10 +24,10 @@
"CREATE_NEW_ACCOUNT": "Yeni hesap oluştur",
"SUBMIT": "Oturum aç",
"SAML": {
"LABEL": "SSO ile giriş yapın",
"LABEL": "Log in via SSO",
"TITLE": "Initiate Single Sign-on (SSO)",
"SUBTITLE": "Enter your work email to access your organization",
"BACK_TO_LOGIN": "Şifre ile giriş yap",
"BACK_TO_LOGIN": "Login via Password",
"WORK_EMAIL": {
"LABEL": "Work Email",
"PLACEHOLDER": "Enter your work email"
@@ -1,10 +1,10 @@
{
"MFA_SETTINGS": {
"TITLE": "İki Faktörlü Kimlik Doğrulama",
"TITLE": "Two-Factor Authentication",
"SUBTITLE": "Secure your account with TOTP-based authentication",
"DESCRIPTION": "Add an extra layer of security to your account using a time-based one-time password (TOTP)",
"STATUS_TITLE": "Kimlik Doğrulama Durumu",
"STATUS_DESCRIPTION": "İki faktörlü kimlik doğrulama ayarlarınızı ve yedek kurtarma kodlarınızı yönetin",
"STATUS_TITLE": "Authentication Status",
"STATUS_DESCRIPTION": "Manage your two-factor authentication settings and backup recovery codes",
"ENABLED": "Etkin",
"DISABLED": "Devre dışı",
"STATUS_ENABLED": "Two-factor authentication is active",
@@ -18,89 +18,89 @@
"STEP1_TITLE": "Scan QR Code with Your Authenticator App",
"STEP1_DESCRIPTION": "Use Google Authenticator, Authy, or any TOTP-compatible app",
"LOADING_QR": "Loading...",
"MANUAL_ENTRY": "Tarama yapamıyor musunuz? Kodu manuel olarak girin",
"SECRET_KEY": "Gizli Anahtar",
"MANUAL_ENTRY": "Can't scan? Enter code manually",
"SECRET_KEY": "Secret Key",
"COPY": "Kopyala",
"ENTER_CODE": "Doğrulama uygulamanızdan 6 haneli kodu girin",
"ENTER_CODE": "Enter the 6-digit code from your authenticator app",
"ENTER_CODE_PLACEHOLDER": "000000",
"VERIFY_BUTTON": "Doğrula & Devam Et",
"VERIFY_BUTTON": "Verify & Continue",
"CANCEL": "İptal Et",
"ERROR_STARTING": "MFA etkinleştirilmemiştir. Lütfen yöneticiyle iletişime geçin.",
"INVALID_CODE": "Geçersiz doğrulama kodu",
"SECRET_COPIED": "Gizli anahtar panoya kopyalandı",
"SUCCESS": "İki faktörlü kimlik doğrulama başarıyla etkinleştirildi"
"ERROR_STARTING": "MFA not enabled. Please contact administrator.",
"INVALID_CODE": "Invalid verification code",
"SECRET_COPIED": "Secret key copied to clipboard",
"SUCCESS": "Two-factor authentication has been enabled successfully"
},
"BACKUP": {
"TITLE": "Yedek Kodlarınızı Kaydedin",
"DESCRIPTION": "Bu kodları güvenli bir yerde saklayın. Kimlik doğrulayıcınıza erişiminizi kaybederseniz, her birini bir kez kullanabilirsiniz.",
"IMPORTANT": "Önemli:",
"IMPORTANT_NOTE": " Bu kodları güvenli bir yerde saklayın. Bir daha göremeyeceksiniz.",
"TITLE": "Save Your Backup Codes",
"DESCRIPTION": "Keep these codes safe. Each can be used once if you lose access to your authenticator",
"IMPORTANT": "Important:",
"IMPORTANT_NOTE": " Save these codes in a secure location. You won't be able to see them again.",
"DOWNLOAD": "İndir",
"COPY_ALL": "Tümünü Kopyala",
"CONFIRM": "Yedek kodlarımı güvenli bir yerde kaydettim ve bunları tekrar göremeyeceğimi anlıyorum",
"COMPLETE_SETUP": "Kurulumu Tamamla",
"CODES_COPIED": "Yedek kodlar panoya kopyalandı"
"COPY_ALL": "Copy All",
"CONFIRM": "I have saved my backup codes in a secure location and understand that I won't be able to see them again",
"COMPLETE_SETUP": "Complete Setup",
"CODES_COPIED": "Backup codes copied to clipboard"
},
"MANAGEMENT": {
"BACKUP_CODES": "Yedek Kodlar",
"BACKUP_CODES_DESC": "Mevcut kodlarınızı kaybettiyseniz veya kullandıysanız yeni kodlar oluşturun",
"REGENERATE": "Yedek Kodları Yeniden Oluştur",
"DISABLE_MFA": "2FA'yı devre dışı bırak",
"DISABLE_MFA_DESC": "Hesabınızdan iki faktörlü kimlik doğrulamayı kaldırın",
"DISABLE_BUTTON": "İki Faktörlü Kimlik Doğrulamayı Devre Dışı Bırak"
"BACKUP_CODES": "Backup Codes",
"BACKUP_CODES_DESC": "Generate new codes if you've lost or used your existing ones",
"REGENERATE": "Regenerate Backup Codes",
"DISABLE_MFA": "Disable 2FA",
"DISABLE_MFA_DESC": "Remove two-factor authentication from your account",
"DISABLE_BUTTON": "Disable Two-Factor Authentication"
},
"DISABLE": {
"TITLE": "İki Faktörlü Kimlik Doğrulamayı Devre Dışı Bırak",
"DESCRIPTION": "İki faktörlü kimlik doğrulamayı devre dışı bırakmak için şifrenizi ve doğrulama kodunu girmeniz gerekir.",
"TITLE": "Disable Two-Factor Authentication",
"DESCRIPTION": "You'll need to enter your password and a verification code to disable two-factor authentication.",
"PASSWORD": "Parola",
"OTP_CODE": "Doğrulama Kodu",
"OTP_CODE": "Verification Code",
"OTP_CODE_PLACEHOLDER": "000000",
"CONFIRM": "2FA'yı devre dışı bırak",
"CONFIRM": "Disable 2FA",
"CANCEL": "İptal Et",
"SUCCESS": "İki faktörlü kimlik doğrulama devre dışı bırakıldı",
"ERROR": "MFA devre dışı bırakılamadı. Lütfen kimlik bilgilerinizi kontrol edin."
"SUCCESS": "Two-factor authentication has been disabled",
"ERROR": "Failed to disable MFA. Please check your credentials."
},
"REGENERATE": {
"TITLE": "Yedek Kodları Yeniden Oluştur",
"DESCRIPTION": "Bu işlem, mevcut yedek kodlarınızı geçersiz kılacak ve yeni kodlar oluşturacaktır. Devam etmek için doğrulama kodunuzu girin.",
"OTP_CODE": "Doğrulama Kodu",
"TITLE": "Regenerate Backup Codes",
"DESCRIPTION": "This will invalidate your existing backup codes and generate new ones. Enter your verification code to continue.",
"OTP_CODE": "Verification Code",
"OTP_CODE_PLACEHOLDER": "000000",
"CONFIRM": "Yeni Kodlar Oluştur",
"CONFIRM": "Generate New Codes",
"CANCEL": "İptal Et",
"NEW_CODES_TITLE": "Yeni Yedek Kodlar Oluşturuldu",
"NEW_CODES_DESC": "Eski yedek kodlarınız geçersiz hale getirilmiştir. Bu yeni kodları güvenli bir yerde saklayın.",
"CODES_IMPORTANT": "Önemli:",
"CODES_IMPORTANT_NOTE": " Her kod yalnızca bir kez kullanılabilir. Bu pencereyi kapatmadan önce kodları kaydedin.",
"DOWNLOAD_CODES": "Kodları İndir",
"COPY_ALL_CODES": "Tüm Kodları Kopyala",
"CODES_SAVED": "Kodlarımı Kaydettim",
"SUCCESS": "Yeni yedek kodlar oluşturuldu",
"ERROR": "Yedek kodları yeniden oluşturulamadı"
"NEW_CODES_TITLE": "New Backup Codes Generated",
"NEW_CODES_DESC": "Your old backup codes have been invalidated. Save these new codes in a secure location.",
"CODES_IMPORTANT": "Important:",
"CODES_IMPORTANT_NOTE": " Each code can only be used once. Save them before closing this window.",
"DOWNLOAD_CODES": "Download Codes",
"COPY_ALL_CODES": "Copy All Codes",
"CODES_SAVED": "I've Saved My Codes",
"SUCCESS": "New backup codes have been generated",
"ERROR": "Failed to regenerate backup codes"
}
},
"MFA_VERIFICATION": {
"TITLE": "İki Faktörlü Kimlik Doğrulama",
"DESCRIPTION": "Devam etmek için doğrulama kodunuzu girin",
"AUTHENTICATOR_APP": "Kimlik Doğrulama Uygulaması",
"BACKUP_CODE": "Yedek Kod",
"ENTER_OTP_CODE": "Doğrulama uygulamasından 6 haneli kodu girin",
"ENTER_BACKUP_CODE": "Yedek kodlarınızdan birini girin",
"TITLE": "Two-Factor Authentication",
"DESCRIPTION": "Enter your verification code to continue",
"AUTHENTICATOR_APP": "Authenticator App",
"BACKUP_CODE": "Backup Code",
"ENTER_OTP_CODE": "Enter 6-digit code from your authenticator app",
"ENTER_BACKUP_CODE": "Enter one of your backup codes",
"BACKUP_CODE_PLACEHOLDER": "000000",
"VERIFY_BUTTON": "Doğrula",
"TRY_ANOTHER_METHOD": "Başka bir doğrulama yöntemi deneyin",
"CANCEL_LOGIN": "İptal et ve giriş sayfasına dön",
"HELP_TEXT": "Giriş yaparken sorun mu yaşıyorsunuz?",
"LEARN_MORE": "2FA hakkında daha fazla bilgi edinin",
"VERIFY_BUTTON": "Verify",
"TRY_ANOTHER_METHOD": "Try another verification method",
"CANCEL_LOGIN": "Cancel and return to login",
"HELP_TEXT": "Having trouble signing in?",
"LEARN_MORE": "Learn more about 2FA",
"HELP_MODAL": {
"TITLE": "İki Faktörlü Kimlik Doğrulama Yardımı",
"AUTHENTICATOR_TITLE": "Kimlik Doğrulama Uygulaması Kullanma",
"AUTHENTICATOR_DESC": "Kimlik doğrulayıcı uygulamanızı (Google Authenticator, Authy vb.) açın ve hesabınız için gösterilen 6 haneli kodu girin.",
"BACKUP_TITLE": "Yedek Kod Kullanma",
"BACKUP_DESC": "Kimlik doğrulayıcı uygulamanıza erişiminiz yoksa, 2FA kurarken kaydettiğiniz yedek kodlardan birini kullanabilirsiniz. Her kod yalnızca bir kez kullanılabilir.",
"CONTACT_TITLE": "Daha Fazla Yardıma mı İhtiyacınız Var?",
"CONTACT_DESC_CLOUD": "Hem kimlik doğrulayıcı uygulamanıza hem de yedek kodlarınıza erişiminizi kaybettiyseniz, yardım için Chatwoot destek ekibiyle iletişime geçin.",
"CONTACT_DESC_SELF_HOSTED": "Hem kimlik doğrulayıcı uygulamanıza hem de yedek kodlarınıza erişiminizi kaybettiyseniz, yardım için yöneticinizle iletişime geçin."
"TITLE": "Two-Factor Authentication Help",
"AUTHENTICATOR_TITLE": "Using an Authenticator App",
"AUTHENTICATOR_DESC": "Open your authenticator app (Google Authenticator, Authy, etc.) and enter the 6-digit code shown for your account.",
"BACKUP_TITLE": "Using a Backup Code",
"BACKUP_DESC": "If you don't have access to your authenticator app, you can use one of the backup codes you saved when setting up 2FA. Each code can only be used once.",
"CONTACT_TITLE": "Need More Help?",
"CONTACT_DESC_CLOUD": "If you've lost access to both your authenticator app and backup codes, please reach out to Chatwoot support for assistance.",
"CONTACT_DESC_SELF_HOSTED": "If you've lost access to both your authenticator app and backup codes, please contact your administrator for assistance."
},
"VERIFICATION_FAILED": "Doğrulama başarısız oldu. Lütfen tekrar deneyin."
"VERIFICATION_FAILED": "Verification failed. Please try again."
}
}
@@ -53,11 +53,11 @@
}
},
"LANGUAGE": {
"TITLE": "Tercih Edilen Dil",
"NOTE": "Kullanmak istediğiniz dili seçin.",
"UPDATE_SUCCESS": "Dil ayarlarınız başarıyla güncellendi",
"UPDATE_ERROR": "Dil ayarlarını güncellerken bir hata oluştu, lütfen tekrar deneyin",
"USE_ACCOUNT_DEFAULT": "Hesap varsayılanını kullan"
"TITLE": "Preferred Language",
"NOTE": "Choose the language you want to use.",
"UPDATE_SUCCESS": "Your Language settings have been updated successfully",
"UPDATE_ERROR": "There is an error while updating the language settings, please try again",
"USE_ACCOUNT_DEFAULT": "Use account default"
}
},
"MESSAGE_SIGNATURE_SECTION": {
@@ -81,9 +81,9 @@
"BTN_TEXT": "Şifre değiştir"
},
"SECURITY_SECTION": {
"TITLE": "Güvenlik",
"NOTE": "Hesabınız için ek güvenlik özelliklerini yönetin.",
"MFA_BUTTON": "İki Faktörlü Kimlik Doğrulamayı Yönet"
"TITLE": "Security",
"NOTE": "Manage additional security features for your account.",
"MFA_BUTTON": "Manage Two-Factor Authentication"
},
"ACCESS_TOKEN": {
"TITLE": "Erişim Jetonu",
@@ -238,7 +238,7 @@
"APPEARANCE": "Change appearance",
"SUPER_ADMIN_CONSOLE": "SuperAdmin console",
"DOCS": "Read documentation",
"CHANGELOG": "Değişiklik Günlüğü",
"CHANGELOG": "Changelog",
"LOGOUT": "Log out"
},
"APP_GLOBAL": {
@@ -342,7 +342,7 @@
"REPORTS_LABEL": "Etiketler",
"REPORTS_INBOX": "Gelen kutusu",
"REPORTS_TEAM": "Ekip",
"AGENT_ASSIGNMENT": "Temsilci Atama",
"AGENT_ASSIGNMENT": "Agent Assignment",
"SET_AVAILABILITY_TITLE": "Kendini şu şekilde ayarla",
"SET_YOUR_AVAILABILITY": "Uygunluk Durumunuzu Ayarlayın",
"SLA": "SLA",
@@ -364,7 +364,7 @@
"INFO_SHORT": "Automatically mark offline when you aren't using the app."
},
"DOCS": "Dokümantasyonu oku",
"SECURITY": "Güvenlik"
"SECURITY": "Security"
},
"BILLING_SETTINGS": {
"TITLE": "Fatura",
@@ -397,8 +397,8 @@
"NO_BILLING_USER": "Fatura hesabınız yapılandırılıyor. Lütfen sayfayı yenileyip tekrar deneyin."
},
"SECURITY_SETTINGS": {
"TITLE": "Güvenlik",
"DESCRIPTION": "Hesap güvenlik ayarlarınızı yönetin.",
"TITLE": "Security",
"DESCRIPTION": "Manage your account security settings.",
"LINK_TEXT": "Learn more about SAML SSO",
"SAML": {
"TITLE": "SAML SSO",
@@ -442,7 +442,7 @@
"VALIDATION": {
"REQUIRED_FIELDS": "SSO URL, Identity Provider Entity ID, and Certificate are required fields",
"SSO_URL_ERROR": "Please enter a valid SSO URL",
"CERTIFICATE_ERROR": "Sertifika gereklidir",
"CERTIFICATE_ERROR": "Certificate is required",
"IDP_ENTITY_ID_ERROR": "Identity Provider Entity ID is required"
},
"ENTERPRISE_PAYWALL": {
@@ -454,7 +454,7 @@
"TITLE": "Upgrade to enable SAML SSO",
"AVAILABLE_ON": "The SAML SSO feature is only available in the Enterprise plans.",
"UPGRADE_PROMPT": "Upgrade your plan to get access to SAML single sign-on and other advanced features.",
"UPGRADE_NOW": "Şimdi yükselt",
"UPGRADE_NOW": "Upgrade now",
"CANCEL_ANYTIME": "You can change or cancel your plan anytime"
},
"ATTRIBUTE_MAPPING": {
@@ -563,7 +563,7 @@
"CONFIRM_ADD_INBOX_DIALOG": {
"TITLE": "Add inbox",
"DESCRIPTION": "{inboxName} inbox is already linked to another policy. Are you sure you want to link it to this policy? It will be unlinked from the other policy.",
"CONFIRM_BUTTON_LABEL": "Devam et",
"CONFIRM_BUTTON_LABEL": "Continue",
"CANCEL_BUTTON_LABEL": "İptal Et"
},
"API": {
@@ -620,7 +620,7 @@
},
"FAIR_DISTRIBUTION": {
"LABEL": "Fair distribution policy",
"DESCRIPTION": "Belirli bir zaman diliminde her bir temsilciye atanabilecek maksimum konuşma sayısını ayarlayarak herhangi bir temsilcinin aşırı yüklenmesini önleyin. Bu zorunlu alanın varsayılan değeri saat başına 100 konuşmadır.",
"DESCRIPTION": "Set the maximum number of conversations that can be assigned per agent within a time window to avoid overloading any one agent. This required field defaults to 100 conversations per hour.",
"INPUT_MAX": "Assign max",
"DURATION": "Conversations per agent in every"
},
@@ -674,7 +674,7 @@
"CONFIRM_ADD_AGENT_DIALOG": {
"TITLE": "Add agent",
"DESCRIPTION": "{agentName} is already linked to another policy. Are you sure you want to link it to this policy? It will be unlinked from the other policy.",
"CONFIRM_BUTTON_LABEL": "Devam et",
"CONFIRM_BUTTON_LABEL": "Continue",
"CANCEL_BUTTON_LABEL": "İptal Et"
},
"API": {
@@ -706,19 +706,19 @@
"ADD_BUTTON": "Add inbox",
"FIELD": {
"SELECT_INBOX": "Select inbox",
"MAX_CONVERSATIONS": "Maksimum Konuşma",
"SET_LIMIT": "Sınır belirle"
"MAX_CONVERSATIONS": "Max conversations",
"SET_LIMIT": "Set limit"
},
"EMPTY_STATE": "Gelen kutusu sınırı belirlenmemiş"
"EMPTY_STATE": "No inbox limit set"
},
"EXCLUSION_RULES": {
"LABEL": "Hariç tutma kuralları",
"LABEL": "Exclusion rules",
"DESCRIPTION": "Conversations that satisfy the following conditions would not count towards agent capacity",
"TAGS": {
"LABEL": "Exclude conversations tagged with specific labels",
"ADD_TAG": "etiket ekle",
"ADD_TAG": "add tag",
"DROPDOWN": {
"SEARCH_PLACEHOLDER": "Eklemek için etiketleri arayın ve seçin"
"SEARCH_PLACEHOLDER": "Search and select tags to add"
},
"EMPTY_STATE": "No tags added to this policy."
},
@@ -3,22 +3,22 @@
"MODAL": {
"TITLE": "WhatsApp Şablonları",
"SUBTITLE": "Göndermek istediğiniz WhatsApp şablonunu seçin",
"TEMPLATE_SELECTED_SUBTITLE": "Şablonu yapılandır: {templateName}"
"TEMPLATE_SELECTED_SUBTITLE": "Configure template: {templateName}"
},
"PICKER": {
"SEARCH_PLACEHOLDER": "Şablon Ara",
"NO_TEMPLATES_FOUND": "İçin hiç şablon bulunamadı",
"HEADER": "Başlık",
"BODY": "Gövde",
"FOOTER": "Altbilgi",
"BUTTONS": "Butonlar",
"HEADER": "Header",
"BODY": "Body",
"FOOTER": "Footer",
"BUTTONS": "Buttons",
"CATEGORY": "Kategori",
"MEDIA_CONTENT": "Medya İçeriği",
"MEDIA_CONTENT_FALLBACK": "medya içeriği",
"NO_TEMPLATES_AVAILABLE": "WhatsApp şablonu mevcut değil. WhatsApp'tan şablonları senkronize etmek için yenile düğmesine tıklayın.",
"REFRESH_BUTTON": "Şablonları yenile",
"REFRESH_SUCCESS": "Şablonların yenilenmesi başlatıldı. Güncelleme birkaç dakika sürebilir.",
"REFRESH_ERROR": "Şablonları yenileme işlemi başarısız oldu. Lütfen tekrar deneyin.",
"MEDIA_CONTENT": "Media Content",
"MEDIA_CONTENT_FALLBACK": "media content",
"NO_TEMPLATES_AVAILABLE": "No WhatsApp templates available. Click refresh to sync templates from WhatsApp.",
"REFRESH_BUTTON": "Refresh templates",
"REFRESH_SUCCESS": "Templates refresh initiated. It may take a couple of minutes to update.",
"REFRESH_ERROR": "Failed to refresh templates. Please try again.",
"LABELS": {
"LANGUAGE": "Dil",
"TEMPLATE_BODY": "Şablon İçeriği",
@@ -33,15 +33,15 @@
"GO_BACK_LABEL": "Geri Git",
"SEND_MESSAGE_LABEL": "Mesaj Gönder",
"FORM_ERROR_MESSAGE": "Lütfen göndermeden önce tüm değişkenleri doldurun",
"MEDIA_HEADER_LABEL": "{type} Başlık",
"OTP_CODE": "4-8 haneli OTP girin",
"EXPIRY_MINUTES": "Süreyi (dakika) girin",
"BUTTON_PARAMETERS": "Buton Parametreleri",
"BUTTON_LABEL": "Buton {index}",
"COUPON_CODE": "Kupon kodunu girin (maks. 15 karakter)",
"MEDIA_URL_LABEL": "{type} URLsini girin",
"DOCUMENT_NAME_PLACEHOLDER": "Belge dosya adını girin (örneğin, Fatura_2025.pdf)",
"BUTTON_PARAMETER": "Buton parametresini girin"
"MEDIA_HEADER_LABEL": "{type} Header",
"OTP_CODE": "Enter 4-8 digit OTP",
"EXPIRY_MINUTES": "Enter expiry minutes",
"BUTTON_PARAMETERS": "Button Parameters",
"BUTTON_LABEL": "Button {index}",
"COUPON_CODE": "Enter coupon code (max 15 chars)",
"MEDIA_URL_LABEL": "Enter {type} URL",
"DOCUMENT_NAME_PLACEHOLDER": "Enter document filename (e.g., Invoice_2025.pdf)",
"BUTTON_PARAMETER": "Enter button parameter"
}
}
}
+3 -35
View File
@@ -1,11 +1,8 @@
<script setup>
import { defineProps, defineModel, computed } from 'vue';
import { useToggle } from '@vueuse/core';
import Button from 'dashboard/components-next/button/Button.vue';
import { defineProps, defineModel } from 'vue';
import WithLabel from './WithLabel.vue';
const props = defineProps({
defineProps({
label: {
type: String,
required: true,
@@ -34,11 +31,6 @@ const props = defineProps({
},
});
const FIELDS = {
TEXT: 'text',
PASSWORD: 'password',
};
defineOptions({
inheritAttrs: false,
});
@@ -47,17 +39,6 @@ const model = defineModel({
type: [String, Number],
required: true,
});
const [isPasswordVisible, togglePasswordVisibility] = useToggle();
const isPasswordField = computed(() => props.type === FIELDS.PASSWORD);
const currentInputType = computed(() => {
if (isPasswordField.value) {
return isPasswordVisible.value ? FIELDS.TEXT : FIELDS.PASSWORD;
}
return props.type;
});
</script>
<template>
@@ -75,7 +56,7 @@ const currentInputType = computed(() => {
v-bind="$attrs"
v-model="model"
:name="name"
:type="currentInputType"
:type="type"
class="block w-full border-none rounded-md shadow-sm bg-n-alpha-black2 appearance-none outline outline-1 focus:outline focus:outline-1 text-n-slate-12 placeholder:text-n-slate-10 sm:text-sm sm:leading-6 px-3 py-3"
:class="{
'error outline-n-ruby-8 dark:outline-n-ruby-8 hover:outline-n-ruby-9 dark:hover:outline-n-ruby-9 disabled:outline-n-ruby-8 dark:disabled:outline-n-ruby-8':
@@ -85,20 +66,7 @@ const currentInputType = computed(() => {
'px-3 py-3': spacing === 'base',
'px-3 py-2 mb-0': spacing === 'compact',
'pl-9': icon,
'pr-10': isPasswordField,
}"
/>
<Button
v-if="isPasswordField"
type="button"
slate
sm
link
:icon="isPasswordVisible ? 'i-lucide-eye-off' : 'i-lucide-eye'"
class="absolute inset-y-0 right-0 pr-3"
:aria-label="isPasswordVisible ? 'Hide password' : 'Show password'"
:aria-pressed="isPasswordVisible"
@click="togglePasswordVisibility()"
/>
</WithLabel>
</template>
@@ -1,6 +1,6 @@
<script>
import { useVuelidate } from '@vuelidate/core';
import { required, minLength, email, sameAs } from '@vuelidate/validators';
import { required, minLength, email } from '@vuelidate/validators';
import { mapGetters } from 'vuex';
import { useAlert } from 'dashboard/composables';
import { DEFAULT_REDIRECT_URL } from 'dashboard/constants/globals';
@@ -8,22 +8,17 @@ import VueHcaptcha from '@hcaptcha/vue3-hcaptcha';
import SimpleDivider from '../../../../../components/Divider/SimpleDivider.vue';
import FormInput from '../../../../../components/Form/Input.vue';
import NextButton from 'dashboard/components-next/button/Button.vue';
import Icon from 'dashboard/components-next/icon/Icon.vue';
import { isValidPassword } from 'shared/helpers/Validators';
import GoogleOAuthButton from '../../../../../components/GoogleOauth/Button.vue';
import { register } from '../../../../../api/auth';
import * as CompanyEmailValidator from 'company-email-validator';
const MIN_PASSWORD_LENGTH = 6;
const SPECIAL_CHAR_REGEX = /[!@#$%^&*()_+\-=[\]{}|'"/\\.,`<>:;?~]/;
export default {
components: {
FormInput,
GoogleOAuthButton,
NextButton,
SimpleDivider,
Icon,
VueHcaptcha,
},
setup() {
@@ -36,7 +31,6 @@ export default {
fullName: '',
email: '',
password: '',
confirmPassword: '',
hCaptchaClientResponse: '',
},
didCaptchaReset: false,
@@ -65,12 +59,7 @@ export default {
password: {
required,
isValidPassword,
minLength: minLength(MIN_PASSWORD_LENGTH),
},
confirmPassword: {
required,
minLength: minLength(MIN_PASSWORD_LENGTH),
sameAsPassword: sameAs(this.credentials.password),
minLength: minLength(6),
},
},
};
@@ -91,11 +80,16 @@ export default {
}
return true;
},
confirmPasswordErrorText() {
const { confirmPassword } = this.v$.credentials;
if (!confirmPassword.$error) return '';
if (confirmPassword.sameAsPassword.$invalid) {
return this.$t('REGISTER.CONFIRM_PASSWORD.ERROR');
passwordErrorText() {
const { password } = this.v$.credentials;
if (!password.$error) {
return '';
}
if (password.minLength.$invalid) {
return this.$t('REGISTER.PASSWORD.ERROR');
}
if (password.isValidPassword.$invalid) {
return this.$t('REGISTER.PASSWORD.IS_INVALID_PASSWORD');
}
return '';
},
@@ -105,51 +99,6 @@ export default {
isFormValid() {
return !this.v$.$invalid && this.hasAValidCaptcha;
},
passwordRequirements() {
const password = this.credentials.password || '';
return {
length: password.length >= MIN_PASSWORD_LENGTH,
uppercase: /[A-Z]/.test(password),
lowercase: /[a-z]/.test(password),
number: /[0-9]/.test(password),
special: SPECIAL_CHAR_REGEX.test(password),
};
},
passwordRequirementItems() {
const reqs = this.passwordRequirements;
return [
{
id: 'length',
met: reqs.length,
label: this.$t('REGISTER.PASSWORD.REQUIREMENTS_LENGTH', {
min: MIN_PASSWORD_LENGTH,
}),
},
{
id: 'uppercase',
met: reqs.uppercase,
label: this.$t('REGISTER.PASSWORD.REQUIREMENTS_UPPERCASE'),
},
{
id: 'lowercase',
met: reqs.lowercase,
label: this.$t('REGISTER.PASSWORD.REQUIREMENTS_LOWERCASE'),
},
{
id: 'number',
met: reqs.number,
label: this.$t('REGISTER.PASSWORD.REQUIREMENTS_NUMBER'),
},
{
id: 'special',
met: reqs.special,
label: this.$t('REGISTER.PASSWORD.REQUIREMENTS_SPECIAL'),
},
];
},
passwordRequirementsMet() {
return Object.values(this.passwordRequirements).every(Boolean);
},
},
methods: {
async submit() {
@@ -177,7 +126,9 @@ export default {
this.v$.$touch();
},
resetCaptcha() {
if (!this.globalConfig.hCaptchaSiteKey) return;
if (!this.globalConfig.hCaptchaSiteKey) {
return;
}
this.$refs.hCaptcha.reset();
this.credentials.hCaptchaClientResponse = '';
this.didCaptchaReset = true;
@@ -232,42 +183,9 @@ export default {
:label="$t('LOGIN.PASSWORD.LABEL')"
:placeholder="$t('SET_NEW_PASSWORD.PASSWORD.PLACEHOLDER')"
:has-error="v$.credentials.password.$error"
aria-describedby="password-requirements"
:error-message="passwordErrorText"
@blur="v$.credentials.password.$touch"
/>
<div
id="password-requirements"
class="text-xs space-y-2 rounded-md px-4 py-3 outline outline-1 outline-n-weak bg-n-alpha-black2"
>
<ul role="list" class="space-y-1 grid grid-cols-2">
<li
v-for="item in passwordRequirementItems"
:key="item.id"
class="flex gap-1 items-center"
>
<Icon
class="flex-none flex-shrink-0 w-3"
:icon="item.met ? 'i-lucide-circle-check-big' : 'i-lucide-circle'"
:class="item.met ? 'text-n-teal-10' : 'text-n-slate-10'"
/>
<span :class="item.met ? 'text-n-slate-11' : 'text-n-slate-10'">
{{ item.label }}
</span>
</li>
</ul>
</div>
<FormInput
v-model="credentials.confirmPassword"
type="password"
name="confirm_password"
:class="{ error: v$.credentials.confirmPassword.$error }"
:label="$t('REGISTER.CONFIRM_PASSWORD.LABEL')"
:placeholder="$t('REGISTER.CONFIRM_PASSWORD.PLACEHOLDER')"
:has-error="v$.credentials.confirmPassword.$error"
:error-message="confirmPasswordErrorText"
@blur="v$.credentials.confirmPassword.$touch"
/>
<div v-if="globalConfig.hCaptchaSiteKey" class="mb-3">
<VueHcaptcha
ref="hCaptcha"
+7 -7
View File
@@ -20,18 +20,18 @@
"TEAM_AVAILABILITY": {
"ONLINE": "Jesteśmy dostępni",
"OFFLINE": "W tej chwili jesteśmy niedostępni",
"BACK_AS_SOON_AS_POSSIBLE": "Wrócimy tak szybko, jak to możliwe"
"BACK_AS_SOON_AS_POSSIBLE": "We will be back as soon as possible"
},
"REPLY_TIME": {
"IN_A_FEW_MINUTES": "Zwykle odpowiadamy w ciągu paru minut",
"IN_A_FEW_HOURS": "Zwykle odpowiadamy w ciągu paru godzin",
"IN_A_DAY": "Zwykle odpowiadamy w przeciągu jednego dnia",
"BACK_IN_HOURS": "Będziemy ponownie dostępni za {n} godzinę | Będziemy ponownie dostępni za {n} godzin",
"BACK_IN_MINUTES": "Będziemy ponownie dostępni za {time} minut",
"BACK_AT_TIME": "Będziemy ponownie dostępni o {time}",
"BACK_ON_DAY": "Będziemy ponownie dostępni w {day}",
"BACK_TOMORROW": "Będziemy ponownie dostępni jutro",
"BACK_IN_SOME_TIME": "Będziemy ponownie dostępni niedługo"
"BACK_IN_HOURS": "We will be back online in {n} hour | We will be back online in {n} hours",
"BACK_IN_MINUTES": "We will be back online in {time} minutes",
"BACK_AT_TIME": "We will be back online at {time}",
"BACK_ON_DAY": "We will be back online on {day}",
"BACK_TOMORROW": "We will be back online tomorrow",
"BACK_IN_SOME_TIME": "We will be back online in some time"
},
"DAY_NAMES": {
"SUNDAY": "Niedziela",
+8 -8
View File
@@ -14,24 +14,24 @@
},
"THUMBNAIL": {
"AUTHOR": {
"NOT_AVAILABLE": "Şu anda müsait değil"
"NOT_AVAILABLE": "Not available"
}
},
"TEAM_AVAILABILITY": {
"ONLINE": "Çevrimiçi",
"OFFLINE": "Şu an operatörlerimiz müsait değil",
"BACK_AS_SOON_AS_POSSIBLE": "En kısa sürede geri döneceğiz"
"BACK_AS_SOON_AS_POSSIBLE": "We will be back as soon as possible"
},
"REPLY_TIME": {
"IN_A_FEW_MINUTES": "Genellikle birkaç dakika içinde yanıt verir",
"IN_A_FEW_HOURS": "Genellikle birkaç saat içinde yanıt verir",
"IN_A_DAY": "Genellikle bir gün içinde yanıtlar",
"BACK_IN_HOURS": "{n} saat içinde çevrimiçi olacağız | {n} saat içinde çevrimiçi olacağız",
"BACK_IN_MINUTES": "{time} dakika içinde çevrimiçi olacağız",
"BACK_AT_TIME": "{time} saatinde çevrimiçi olacağız",
"BACK_ON_DAY": "{day} günü çevrimiçi olacağız",
"BACK_TOMORROW": "Yarın çevrimiçi olacağız",
"BACK_IN_SOME_TIME": "Bir süre sonra çevrimiçi olacağız"
"BACK_IN_HOURS": "We will be back online in {n} hour | We will be back online in {n} hours",
"BACK_IN_MINUTES": "We will be back online in {time} minutes",
"BACK_AT_TIME": "We will be back online at {time}",
"BACK_ON_DAY": "We will be back online on {day}",
"BACK_TOMORROW": "We will be back online tomorrow",
"BACK_IN_SOME_TIME": "We will be back online in some time"
},
"DAY_NAMES": {
"SUNDAY": "Pazar",
@@ -44,6 +44,13 @@ class Twilio::IncomingMessageService
twilio_channel.sms? ? params[:From] : params[:From].gsub('whatsapp:', '')
end
def normalized_phone_number
return phone_number unless twilio_channel.whatsapp?
# Use the generic normalization service for Twilio WhatsApp
Whatsapp::PhoneNumberNormalizationService.new(inbox).normalize_and_find_contact_by_provider("whatsapp:#{phone_number}", :twilio)
end
def formatted_phone_number
TelephoneNumber.parse(phone_number).international_number
end
@@ -53,8 +60,10 @@ class Twilio::IncomingMessageService
end
def set_contact
source_id = twilio_channel.whatsapp? ? normalized_phone_number : params[:From]
contact_inbox = ::ContactInboxWithContactBuilder.new(
source_id: params[:From],
source_id: source_id,
inbox: inbox,
contact_attributes: contact_attributes
).perform
@@ -48,7 +48,7 @@ module Whatsapp::IncomingMessageServiceHelpers
end
def processed_waid(waid)
Whatsapp::PhoneNumberNormalizationService.new(inbox).normalize_and_find_contact(waid)
Whatsapp::PhoneNumberNormalizationService.new(inbox).normalize_and_find_contact_by_provider(waid, :cloud)
end
def error_webhook_event?(message)
@@ -0,0 +1,13 @@
class Whatsapp::PhoneNormalizers::ArgentinaPhoneNormalizer < Whatsapp::PhoneNormalizers::BasePhoneNormalizer
def handles_country?(waid)
waid.start_with?('549')
end
def normalize(waid)
return waid unless handles_country?(waid)
# Remove the '9' after country code for Argentina numbers
# 5491123456789 -> 541123456789
waid.sub(/^549/, '54')
end
end
@@ -1,23 +1,40 @@
# Service to handle phone number normalization for WhatsApp messages
# Currently supports Brazil phone number format variations
# Currently supports Brazil and Argentina phone number format variations
# Supports both WhatsApp Cloud API and Twilio WhatsApp providers
# Designed to be extensible for additional countries in future PRs
#
# Usage: Whatsapp::PhoneNumberNormalizationService.new(inbox).normalize_and_find_contact(waid)
# Usage:
# Whatsapp::PhoneNumberNormalizationService.new(inbox).normalize_and_find_contact_by_provider(number, :cloud)
# Whatsapp::PhoneNumberNormalizationService.new(inbox).normalize_and_find_contact_by_provider(number, :twilio)
class Whatsapp::PhoneNumberNormalizationService
def initialize(inbox)
@inbox = inbox
end
# Main entry point for phone number normalization
# Returns the source_id of an existing contact if found, otherwise returns original waid
def normalize_and_find_contact(waid)
normalizer = find_normalizer_for_country(waid)
return waid unless normalizer
# Generic method to handle both WhatsApp Cloud and Twilio formats
# Extracts clean number, normalizes it, and returns appropriate format for provider
#
# @param raw_number [String] The phone number in provider-specific format
# - Cloud: "919745786257" (clean number)
# - Twilio: "whatsapp:+919745786257" (prefixed format)
# @param provider [Symbol] :cloud or :twilio
# @return [String] Normalized source_id in provider format or original if not found
def normalize_and_find_contact_by_provider(raw_number, provider)
# Extract clean number based on provider format
clean_number = extract_clean_number(raw_number, provider)
normalized_waid = normalizer.normalize(waid)
existing_contact_inbox = find_existing_contact_inbox(normalized_waid)
# Find appropriate normalizer for the country
normalizer = find_normalizer_for_country(clean_number)
return raw_number unless normalizer
existing_contact_inbox&.source_id || waid
# Normalize the clean number
normalized_clean_number = normalizer.normalize(clean_number)
# Format for provider and check for existing contact
provider_format = format_for_provider(normalized_clean_number, provider)
existing_contact_inbox = find_existing_contact_inbox(provider_format)
existing_contact_inbox&.source_id || raw_number
end
private
@@ -33,7 +50,32 @@ class Whatsapp::PhoneNumberNormalizationService
inbox.contact_inboxes.find_by(source_id: normalized_waid)
end
# Extract clean number from provider-specific format
def extract_clean_number(raw_number, provider)
case provider
when :cloud
raw_number # Already clean: "919745786257"
when :twilio
raw_number.gsub(/^whatsapp:\+/, '') # Remove prefix: "whatsapp:+919745786257" → "919745786257"
else
raise ArgumentError, "Unsupported provider: #{provider}. Use :cloud or :twilio"
end
end
# Format normalized number for provider-specific storage
def format_for_provider(clean_number, provider)
case provider
when :cloud
clean_number # Keep clean: "919745786257"
when :twilio
"whatsapp:+#{clean_number}" # Add prefix: "919745786257" → "whatsapp:+919745786257"
else
raise ArgumentError, "Unsupported provider: #{provider}. Use :cloud or :twilio"
end
end
NORMALIZERS = [
Whatsapp::PhoneNormalizers::BrazilPhoneNormalizer
Whatsapp::PhoneNormalizers::BrazilPhoneNormalizer,
Whatsapp::PhoneNormalizers::ArgentinaPhoneNormalizer
].freeze
end
@@ -5,6 +5,6 @@ end
json.payload do
json.array! @contacts do |contact|
json.partial! 'api/v1/models/contact', formats: [:json], resource: contact, with_contact_inboxes: @include_contact_inboxes
json.partial! 'api/v1/models/contact', formats: [:json], resource: contact, with_contact_inboxes: true
end
end
+25 -25
View File
@@ -26,7 +26,7 @@ tr:
auth:
saml:
invalid_email: 'Lütfen geçerli bir e-posta adresi girin'
authentication_failed: 'Kimlik doğrulama başarısız oldu. Lütfen kimlik bilgilerinizi kontrol edin ve tekrar deneyin.'
authentication_failed: 'Authentication failed. Please check your credentials and try again.'
messages:
reset_password_success: Parola sıfırlama isteği başarılı. Talimatlar için postanızı kontrol edin.
reset_password_failure: Belirtilen e-postaya sahip herhangi bir kullanıcı bulamadık.
@@ -94,19 +94,19 @@ tr:
custom_attribute_definition:
key_conflict: The provided key is not allowed as it might conflict with default attributes.
mfa:
already_enabled: MFA zaten etkinleştirilmiştir
not_enabled: MFA etkinleştirilmemiştir
invalid_code: Geçersiz doğrulama kodu
invalid_backup_code: Geçersiz yedek kod
invalid_token: Geçersiz veya süresi dolmuş MFA jetonu
invalid_credentials: Geçersiz kimlik bilgileri veya doğrulama kodu
feature_unavailable: MFA özelliği kullanılamıyor. Lütfen şifreleme anahtarlarını yapılandırın.
already_enabled: MFA is already enabled
not_enabled: MFA is not enabled
invalid_code: Invalid verification code
invalid_backup_code: Invalid backup code
invalid_token: Invalid or expired MFA token
invalid_credentials: Invalid credentials or verification code
feature_unavailable: MFA feature is not available. Please configure encryption keys.
profile:
mfa:
enabled: MFA başarıyla etkinleştirildi
disabled: MFA başarıyla devre dışı bırakıldı
enabled: MFA enabled successfully
disabled: MFA disabled successfully
account_saml_settings:
invalid_certificate: PEM formatında geçerli bir X.509 sertifikası olmalıdır
invalid_certificate: must be a valid X.509 certificate in PEM format
reports:
period: Raporlama aralığı %{since}'dan %{until}'a
utc_warning: Oluşturulan rapor UTC zaman dilimindedir.
@@ -129,7 +129,7 @@ tr:
avg_first_response_time: Avg first response time
avg_resolution_time: Avg resolution time
avg_reply_time: Ortalama yanıt süresi
resolution_count: Çözüm Sayısı
resolution_count: Çözünürlük Sayısı
team_csv:
team_name: Ekip adı
conversations_count: Konuşma sayısı
@@ -220,7 +220,7 @@ tr:
csat:
not_sent_due_to_messaging_window: 'Giden mesaj kısıtlamaları nedeniyle CSAT anketi gönderilmedi'
auto_resolve:
not_sent_due_to_messaging_window: 'Giden mesaj kısıtlamaları nedeniyle otomatik çözüm mesajı gönderilmedi'
not_sent_due_to_messaging_window: 'Auto-resolve message not sent due to outgoing message restrictions'
muted: '%{user_name}, sohbeti sessize aldı'
unmuted: '%{user_name} konuşmanın sesini açtı'
auto_resolution_message: 'Resolving the conversation as it has been inactive for a while. Please start a new conversation if you need further assistance.'
@@ -303,21 +303,21 @@ tr:
invalid_tool_call: 'Geçersiz araç çağrısı'
tool_not_available: 'Araç mevcut değil'
documents:
limit_exceeded: 'Belge sınırı aşıldı'
pdf_format_error: 'PDF dosyası olmalıdır'
pdf_size_error: '10 MB''dan az olmalıdır'
pdf_upload_failed: 'OpenAI''ye PDF yükleme başarısız'
pdf_upload_success: 'PDF başarıyla yüklendi, file_id: %{file_id}'
pdf_processing_failed: 'PDF belgesi işlenemedi %{document_id}: %{error}'
pdf_processing_success: 'PDF belgesi başarıyla işlendi %{document_id}'
limit_exceeded: 'Document limit exceeded'
pdf_format_error: 'must be a PDF file'
pdf_size_error: 'must be less than 10MB'
pdf_upload_failed: 'Failed to upload PDF to OpenAI'
pdf_upload_success: 'PDF uploaded successfully with file_id: %{file_id}'
pdf_processing_failed: 'Failed to process PDF document %{document_id}: %{error}'
pdf_processing_success: 'Successfully processed PDF document %{document_id}'
faq_generation_complete: 'FAQ generation complete. Total FAQs created: %{count}'
using_paginated_faq: 'Using paginated FAQ generation for document %{document_id}'
using_standard_faq: 'Using standard FAQ generation for document %{document_id}'
response_creation_error: 'Error in creating response document: %{error}'
missing_openai_file_id: 'Document must have openai_file_id for paginated processing'
openai_api_error: 'OpenAI API Hatası: %{error}'
openai_api_error: 'OpenAI API Error: %{error}'
starting_paginated_faq: 'Starting paginated FAQ generation (%{pages_per_chunk} pages per chunk)'
stopping_faq_generation: 'İşlem durduruldu. Sebep: %{reason}'
stopping_faq_generation: 'Stopping processing. Reason: %{reason}'
paginated_faq_complete: 'Paginated generation complete. Total FAQs: %{total_faqs}, Pages processed: %{pages_processed}'
processing_pages: 'Processing pages %{start}-%{end} (iteration %{iteration})'
chunk_generated: 'Chunk generated %{chunk_faqs} FAQs. Total so far: %{total_faqs}'
@@ -407,9 +407,9 @@ tr:
portals:
send_instructions:
email_required: 'E-posta gereklidir'
invalid_email_format: 'Geçersiz e-posta formatı'
custom_domain_not_configured: 'Özel alan adı yapılandırılmadı'
invalid_email_format: 'Invalid email format'
custom_domain_not_configured: 'Custom domain is not configured'
instructions_sent_successfully: 'Instructions sent successfully'
subject: '%{custom_domain} kurulumunu tamamlayın'
subject: 'Finish setting up %{custom_domain}'
ssl_status:
custom_domain_not_configured: 'Özel alan adı yapılandırılmamış'
+3 -21
View File
@@ -24,31 +24,13 @@ class Integrations::Slack::ChannelBuilder
end
def channels
# Split channel fetching into separate API calls to avoid rate limiting issues.
# Slack's API handles single-type requests (public OR private) much more efficiently
# than mixed-type requests (public AND private). This approach eliminates rate limits
# that occur when requesting both channel types simultaneously.
channel_list = []
# Step 1: Fetch all private channels in one call (expect very few)
private_channels = fetch_channels_by_type('private_channel')
channel_list.concat(private_channels)
# Step 2: Fetch public channels with pagination
public_channels = fetch_channels_by_type('public_channel')
channel_list.concat(public_channels)
channel_list
end
def fetch_channels_by_type(channel_type, limit: 1000)
conversations_list = slack_client.conversations_list(types: channel_type, exclude_archived: true, limit: limit)
conversations_list = slack_client.conversations_list(types: 'public_channel,private_channel', exclude_archived: true)
channel_list = conversations_list.channels
while conversations_list.response_metadata.next_cursor.present?
conversations_list = slack_client.conversations_list(
cursor: conversations_list.response_metadata.next_cursor,
types: channel_type,
exclude_archived: true,
limit: limit
types: 'public_channel,private_channel',
exclude_archived: true
)
channel_list.concat(conversations_list.channels)
end
+4
View File
@@ -13,5 +13,9 @@ FactoryBot.define do
sequence(:phone_number) { |n| "+123456789#{n}1" }
messaging_service_sid { nil }
end
trait :whatsapp do
medium { :whatsapp }
end
end
end
+1 -1
View File
@@ -215,7 +215,7 @@ RSpec.describe Conversation do
it 'adds a message for system auto resolution if marked resolved by system' do
account.update(auto_resolve_after: 40 * 24 * 60)
conversation2 = create(:conversation, status: 'open', account: account, assignee: old_assignee)
Current.reset
Current.user = nil
message_data = if account.auto_resolve_after >= 1440 && account.auto_resolve_after % 1440 == 0
{ key: 'auto_resolved_days', count: account.auto_resolve_after / 1440 }
@@ -402,6 +402,230 @@ describe Twilio::IncomingMessageService do
existing_contact.reload
expect(existing_contact.name).to eq('Alice Johnson')
end
describe 'When the incoming number is a Brazilian number in new format with 9 included' do
let!(:whatsapp_twilio_channel) do
create(:channel_twilio_sms, :whatsapp, account: account, account_sid: 'ACxxx',
inbox: create(:inbox, account: account, greeting_enabled: false))
end
it 'creates appropriate conversations, message and contacts if contact does not exist' do
params = {
SmsSid: 'SMxx',
From: 'whatsapp:+5541988887777',
AccountSid: 'ACxxx',
MessagingServiceSid: whatsapp_twilio_channel.messaging_service_sid,
Body: 'Test message from Brazil',
ProfileName: 'João Silva'
}
described_class.new(params: params).perform
expect(whatsapp_twilio_channel.inbox.conversations.count).not_to eq(0)
expect(whatsapp_twilio_channel.inbox.contacts.first.name).to eq('João Silva')
expect(whatsapp_twilio_channel.inbox.messages.first.content).to eq('Test message from Brazil')
expect(whatsapp_twilio_channel.inbox.contact_inboxes.first.source_id).to eq('whatsapp:+5541988887777')
end
it 'appends to existing contact if contact inbox exists' do
# Create existing contact with same format
normalized_contact = create(:contact, account: account, phone_number: '+5541988887777')
contact_inbox = create(:contact_inbox, source_id: 'whatsapp:+5541988887777', contact: normalized_contact,
inbox: whatsapp_twilio_channel.inbox)
last_conversation = create(:conversation, inbox: whatsapp_twilio_channel.inbox, contact_inbox: contact_inbox)
params = {
SmsSid: 'SMxx',
From: 'whatsapp:+5541988887777',
AccountSid: 'ACxxx',
MessagingServiceSid: whatsapp_twilio_channel.messaging_service_sid,
Body: 'Another message from Brazil',
ProfileName: 'João Silva'
}
described_class.new(params: params).perform
# No new conversation should be created
expect(whatsapp_twilio_channel.inbox.conversations.count).to eq(1)
# Message appended to the last conversation
expect(last_conversation.messages.last.content).to eq('Another message from Brazil')
end
end
describe 'When incoming number is a Brazilian number in old format without the 9 included' do
let!(:whatsapp_twilio_channel) do
create(:channel_twilio_sms, :whatsapp, account: account, account_sid: 'ACxxx',
inbox: create(:inbox, account: account, greeting_enabled: false))
end
it 'appends to existing contact when contact inbox exists in old format' do
# Create existing contact with old format (12 digits)
old_contact = create(:contact, account: account, phone_number: '+554188887777')
contact_inbox = create(:contact_inbox, source_id: 'whatsapp:+554188887777', contact: old_contact, inbox: whatsapp_twilio_channel.inbox)
last_conversation = create(:conversation, inbox: whatsapp_twilio_channel.inbox, contact_inbox: contact_inbox)
params = {
SmsSid: 'SMxx',
From: 'whatsapp:+554188887777',
AccountSid: 'ACxxx',
MessagingServiceSid: whatsapp_twilio_channel.messaging_service_sid,
Body: 'Test message from Brazil old format',
ProfileName: 'Maria Silva'
}
described_class.new(params: params).perform
# No new conversation should be created
expect(whatsapp_twilio_channel.inbox.conversations.count).to eq(1)
# Message appended to the last conversation
expect(last_conversation.messages.last.content).to eq('Test message from Brazil old format')
end
it 'appends to existing contact when contact inbox exists in new format' do
# Create existing contact with new format (13 digits)
normalized_contact = create(:contact, account: account, phone_number: '+5541988887777')
contact_inbox = create(:contact_inbox, source_id: 'whatsapp:+5541988887777', contact: normalized_contact,
inbox: whatsapp_twilio_channel.inbox)
last_conversation = create(:conversation, inbox: whatsapp_twilio_channel.inbox, contact_inbox: contact_inbox)
# Incoming message with old format (12 digits)
params = {
SmsSid: 'SMxx',
From: 'whatsapp:+554188887777',
AccountSid: 'ACxxx',
MessagingServiceSid: whatsapp_twilio_channel.messaging_service_sid,
Body: 'Test message from Brazil',
ProfileName: 'João Silva'
}
described_class.new(params: params).perform
# Should find and use existing contact, not create duplicate
expect(whatsapp_twilio_channel.inbox.conversations.count).to eq(1)
# Message appended to the existing conversation
expect(last_conversation.messages.last.content).to eq('Test message from Brazil')
# Should use the existing contact's source_id (normalized format)
expect(whatsapp_twilio_channel.inbox.contact_inboxes.first.source_id).to eq('whatsapp:+5541988887777')
end
it 'creates contact inbox with incoming number when no existing contact' do
params = {
SmsSid: 'SMxx',
From: 'whatsapp:+554188887777',
AccountSid: 'ACxxx',
MessagingServiceSid: whatsapp_twilio_channel.messaging_service_sid,
Body: 'Test message from Brazil',
ProfileName: 'Carlos Silva'
}
described_class.new(params: params).perform
expect(whatsapp_twilio_channel.inbox.conversations.count).not_to eq(0)
expect(whatsapp_twilio_channel.inbox.contacts.first.name).to eq('Carlos Silva')
expect(whatsapp_twilio_channel.inbox.messages.first.content).to eq('Test message from Brazil')
expect(whatsapp_twilio_channel.inbox.contact_inboxes.first.source_id).to eq('whatsapp:+554188887777')
end
end
describe 'When the incoming number is an Argentine number with 9 after country code' do
let!(:whatsapp_twilio_channel) do
create(:channel_twilio_sms, :whatsapp, account: account, account_sid: 'ACxxx',
inbox: create(:inbox, account: account, greeting_enabled: false))
end
it 'creates appropriate conversations, message and contacts if contact does not exist' do
params = {
SmsSid: 'SMxx',
From: 'whatsapp:+5491123456789',
AccountSid: 'ACxxx',
MessagingServiceSid: whatsapp_twilio_channel.messaging_service_sid,
Body: 'Test message from Argentina',
ProfileName: 'Carlos Mendoza'
}
described_class.new(params: params).perform
expect(whatsapp_twilio_channel.inbox.conversations.count).not_to eq(0)
expect(whatsapp_twilio_channel.inbox.contacts.first.name).to eq('Carlos Mendoza')
expect(whatsapp_twilio_channel.inbox.messages.first.content).to eq('Test message from Argentina')
expect(whatsapp_twilio_channel.inbox.contact_inboxes.first.source_id).to eq('whatsapp:+5491123456789')
end
it 'appends to existing contact if contact inbox exists with normalized format' do
# Create existing contact with normalized format (without 9 after country code)
normalized_contact = create(:contact, account: account, phone_number: '+541123456789')
contact_inbox = create(:contact_inbox, source_id: 'whatsapp:+541123456789', contact: normalized_contact,
inbox: whatsapp_twilio_channel.inbox)
last_conversation = create(:conversation, inbox: whatsapp_twilio_channel.inbox, contact_inbox: contact_inbox)
# Incoming message with 9 after country code
params = {
SmsSid: 'SMxx',
From: 'whatsapp:+5491123456789',
AccountSid: 'ACxxx',
MessagingServiceSid: whatsapp_twilio_channel.messaging_service_sid,
Body: 'Test message from Argentina',
ProfileName: 'Carlos Mendoza'
}
described_class.new(params: params).perform
# Should find and use existing contact, not create duplicate
expect(whatsapp_twilio_channel.inbox.conversations.count).to eq(1)
# Message appended to the existing conversation
expect(last_conversation.messages.last.content).to eq('Test message from Argentina')
# Should use the normalized source_id from existing contact
expect(whatsapp_twilio_channel.inbox.contact_inboxes.first.source_id).to eq('whatsapp:+541123456789')
end
end
describe 'When incoming number is an Argentine number without 9 after country code' do
let!(:whatsapp_twilio_channel) do
create(:channel_twilio_sms, :whatsapp, account: account, account_sid: 'ACxxx',
inbox: create(:inbox, account: account, greeting_enabled: false))
end
it 'appends to existing contact when contact inbox exists with same format' do
# Create existing contact with same format (without 9)
contact = create(:contact, account: account, phone_number: '+541123456789')
contact_inbox = create(:contact_inbox, source_id: 'whatsapp:+541123456789', contact: contact, inbox: whatsapp_twilio_channel.inbox)
last_conversation = create(:conversation, inbox: whatsapp_twilio_channel.inbox, contact_inbox: contact_inbox)
params = {
SmsSid: 'SMxx',
From: 'whatsapp:+541123456789',
AccountSid: 'ACxxx',
MessagingServiceSid: whatsapp_twilio_channel.messaging_service_sid,
Body: 'Test message from Argentina',
ProfileName: 'Ana García'
}
described_class.new(params: params).perform
# No new conversation should be created
expect(whatsapp_twilio_channel.inbox.conversations.count).to eq(1)
# Message appended to the last conversation
expect(last_conversation.messages.last.content).to eq('Test message from Argentina')
end
it 'creates contact inbox with incoming number when no existing contact' do
params = {
SmsSid: 'SMxx',
From: 'whatsapp:+541123456789',
AccountSid: 'ACxxx',
MessagingServiceSid: whatsapp_twilio_channel.messaging_service_sid,
Body: 'Test message from Argentina',
ProfileName: 'Diego López'
}
described_class.new(params: params).perform
expect(whatsapp_twilio_channel.inbox.conversations.count).not_to eq(0)
expect(whatsapp_twilio_channel.inbox.contacts.first.name).to eq('Diego López')
expect(whatsapp_twilio_channel.inbox.messages.first.content).to eq('Test message from Argentina')
expect(whatsapp_twilio_channel.inbox.contact_inboxes.first.source_id).to eq('whatsapp:+541123456789')
end
end
end
end
end
@@ -0,0 +1,95 @@
require 'rails_helper'
RSpec.describe Whatsapp::PhoneNumberNormalizationService do
let(:account) { create(:account) }
let(:inbox) { create(:inbox, account: account) }
let(:service) { described_class.new(inbox) }
describe '#normalize_and_find_contact_by_provider' do
context 'when handling Brazilian numbers' do
context 'with WhatsApp Cloud provider' do
it 'normalizes old format and finds existing contact with new format' do
# Create existing contact with new format (13 digits)
create(:contact_inbox, inbox: inbox, source_id: '5541988887777')
# Incoming old format (12 digits)
result = service.normalize_and_find_contact_by_provider('554188887777', :cloud)
expect(result).to eq('5541988887777')
end
it 'returns original if no existing contact found' do
result = service.normalize_and_find_contact_by_provider('554188887777', :cloud)
expect(result).to eq('554188887777')
end
end
context 'with Twilio provider' do
it 'normalizes old format and finds existing contact with new format' do
# Create existing contact with new format (Twilio format with 13 digits)
create(:contact_inbox, inbox: inbox, source_id: 'whatsapp:+5541988887777')
# Incoming old format (Twilio format with 12 digits)
result = service.normalize_and_find_contact_by_provider('whatsapp:+554188887777', :twilio)
expect(result).to eq('whatsapp:+5541988887777')
end
it 'returns original if no existing contact found' do
result = service.normalize_and_find_contact_by_provider('whatsapp:+554188887777', :twilio)
expect(result).to eq('whatsapp:+554188887777')
end
end
end
context 'when handling Argentine numbers' do
context 'with WhatsApp Cloud provider' do
it 'normalizes number with 9 and finds existing contact without 9' do
# Create existing contact with normalized format (without 9)
create(:contact_inbox, inbox: inbox, source_id: '541123456789')
# Incoming format with 9 after country code
result = service.normalize_and_find_contact_by_provider('5491123456789', :cloud)
expect(result).to eq('541123456789')
end
end
context 'with Twilio provider' do
it 'normalizes number with 9 and finds existing contact without 9' do
# Create existing contact with normalized format (Twilio format without 9)
create(:contact_inbox, inbox: inbox, source_id: 'whatsapp:+541123456789')
# Incoming format with 9 after country code (Twilio format)
result = service.normalize_and_find_contact_by_provider('whatsapp:+5491123456789', :twilio)
expect(result).to eq('whatsapp:+541123456789')
end
end
end
context 'when handling unsupported countries' do
it 'returns original number for WhatsApp Cloud' do
result = service.normalize_and_find_contact_by_provider('919876543210', :cloud)
expect(result).to eq('919876543210')
end
it 'returns original number for Twilio' do
result = service.normalize_and_find_contact_by_provider('whatsapp:+919876543210', :twilio)
expect(result).to eq('whatsapp:+919876543210')
end
end
context 'when handling invalid provider' do
it 'raises ArgumentError for unsupported provider' do
expect do
service.normalize_and_find_contact_by_provider('919876543210', :unknown)
end.to raise_error(ArgumentError, 'Unsupported provider: unknown. Use :cloud or :twilio')
end
end
end
end