Compare commits

..
Author SHA1 Message Date
406a470c81 feat: Log push notification error (#12543)
Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
2025-09-29 15:19:45 +05:30
Muhsin KelothandGitHub 6c6aaf573c fix: Optimize Slack channel fetching to avoid rate limiting issues (#12542)
### Problem
The Slack integration fails when fetching channels from workspaces with
many channels due to rate limiting errors. The current implementation
makes a single API call requesting both public and private channels
simultaneously with `types: 'public_channel,private_channel'`, which
causes Slack's API to apply complex filtering and hit rate limits more
frequently.

  When testing with a csutomer workspace containing 157 channels:
- Combined request: Hit rate limits after a few pages, required 22+ API
calls with long delays
- Separate requests: Private channels (1 channel) load instantly, public
channels (185 channels) load quickly
  
 
  ### Solution

  Split the channel fetching into two sequential steps:
1. **Fetch private channels first** with `limit: 1000` (expects very
few)
  2. **Fetch public channels second** with pagination as needed

This approach leverages the fact that Slack's API handles single-type
requests much more efficiently than mixed-type requests, avoiding the
rate limiting issues entirely while maintaining the same functionality.
2025-09-29 14:41:48 +05:30
Chatwoot BotandGitHub 487209574c chore: Update translations (#12535) 2025-09-29 11:16:22 +05:30
7f1671c083 feat: Form validation message for password input (#11705)
Fixes https://github.com/chatwoot/chatwoot/issues/10914

# Pull Request Template

## Description

Please include a summary of the change and issue(s) fixed. Also, mention
relevant motivation, context, and any dependencies that this change
requires.
Fixes # (issue)

## Type of change

Please delete options that are not relevant.

- [x] Bug fix (non-breaking change which fixes an issue)
- [x] New feature (non-breaking change which adds functionality)
- [] Breaking change (fix or feature that would cause existing
functionality not to work as expected)
- [ ] This change requires a documentation update

## How Has This Been Tested?

Please describe the tests that you ran to verify your changes. Provide
instructions so we can reproduce. Please also list any relevant details
for your test configuration.


## Checklist:

- [x ] My code follows the style guidelines of this project
- [ ] I have performed a self-review of my code
- [ ] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [ ] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective or that my
feature works
- [ ] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream
modules

---------

Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
Co-authored-by: iamsivin <iamsivin@gmail.com>
2025-09-29 11:12:04 +05:30
b00261d7c2 feat: Add password visibility toggle to form input (#12524)
# Pull Request Template

## Description

This PR adds a password visibility toggle to the auth form input
component.

## Type of change

- [x] New feature (non-breaking change which adds functionality)

## How Has This Been Tested?

### Screencast


https://github.com/user-attachments/assets/17652e86-e823-46e6-a3ba-80af37c78906




## Checklist:

- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [ ] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream
modules

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-09-25 20:14:36 +05:30
fcb91ab88a fix: Auto resolution flaky spec (#11964)
The test was failing because Current.contact was not being cleared when
testing system auto-resolution. Added Current.contact = nil to ensure
the system auto-resolution message is triggered instead of contact
resolution.

🤖 Generated with [Claude Code](https://claude.ai/code)

# Pull Request Template

## Description

Please include a summary of the change and issue(s) fixed. Also, mention
relevant motivation, context, and any dependencies that this change
requires.
Fixes # (issue)

## Type of change

Please delete options that are not relevant.

- [ ] Bug fix (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing
functionality not to work as expected)
- [ ] This change requires a documentation update

## How Has This Been Tested?

Please describe the tests that you ran to verify your changes. Provide
instructions so we can reproduce. Please also list any relevant details
for your test configuration.


## Checklist:

- [ ] My code follows the style guidelines of this project
- [ ] I have performed a self-review of my code
- [ ] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [ ] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective or that my
feature works
- [ ] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream
modules

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
2025-09-25 19:36:38 +05:30
59f7c8aa55 perf: Contact optimisation fixes (#12016)
- Avoids the duplicate count queries for contact end point

Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
2025-09-25 18:59:55 +05:30
34 changed files with 510 additions and 755 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,13 +133,14 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController
end
def fetch_contacts(contacts)
contacts_with_avatar = filtrate(contacts)
.includes([{ avatar_attachment: [:blob] }])
.page(@current_page).per(RESULTS_PER_PAGE)
# 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
return contacts_with_avatar.includes([{ contact_inboxes: [:inbox] }]) if @include_contact_inboxes
contacts_with_avatar
filtrate(contacts)
.includes(includes_hash)
.page(@current_page)
.per(RESULTS_PER_PAGE)
end
def build_contact_inbox
@@ -68,13 +68,17 @@ export const registerSubscription = (onSuccess = () => {}) => {
.then(() => {
onSuccess();
})
.catch(() => {
.catch(error => {
// eslint-disable-next-line no-console
console.error('Push subscription registration failed:', error);
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": "Log in via SSO",
"LABEL": "Login via SSO",
"TITLE": "Initiate Single Sign-on (SSO)",
"SUBTITLE": "Enter your work email to access your organization",
"BACK_TO_LOGIN": "Login via Password",
@@ -27,15 +27,20 @@
"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."
"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"
},
"CONFIRM_PASSWORD": {
"LABEL": "Confirm password",
"PLACEHOLDER": "Confirm password",
"ERROR": "Password doesnot match."
"ERROR": "Passwords do not match."
},
"API": {
"SUCCESS_MESSAGE": "Registration Successfull",
"SUCCESS_MESSAGE": "Registration Successful",
"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": "Create campaign",
"NEW_CAMPAIGN": "Kampanya Oluştur",
"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": "Select a template",
"INFO": "Select a template to use for this campaign.",
"PLACEHOLDER": "Şablon seçin",
"INFO": "Bu kampanya için kullanılacak şablonu seçin.",
"ERROR": "Template is required",
"PREVIEW_TITLE": "{templateName} işleniyor",
"LANGUAGE": "Dil",
@@ -51,6 +51,6 @@
"PLACEHOLDER": "Süre girin"
},
"CHANNEL_SELECTOR": {
"COMING_SOON": "Coming Soon!"
"COMING_SOON": "Çok yakında!"
}
}
@@ -18,9 +18,9 @@
"CREATED_AT_LABEL": "Oluşturuldu",
"NEW_MESSAGE": "Yeni Mesaj",
"CALL": "Ara",
"CALL_UNDER_DEVELOPMENT": "Calling is under development",
"CALL_UNDER_DEVELOPMENT": "Arama özelliği geliştirme aşamasındadır",
"VOICE_INBOX_PICKER": {
"TITLE": "Choose a voice inbox"
"TITLE": "Sesli gelen kutusu seçin"
},
"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": "Add contact note",
"ADD_NOTE": "Kişi notu ekle",
"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": "There are no notes yet. Use the Add note button to create one."
"CONVERSATION_EMPTY_STATE": "Henüz not yok. Not eklemek için Not ekle düğmesini kullanın."
}
},
"EMPTY_STATE": {
@@ -3,30 +3,30 @@
"MODAL": {
"TITLE": "Twilio Templates",
"SUBTITLE": "Select the Twilio template you want to send",
"TEMPLATE_SELECTED_SUBTITLE": "Configure template: {templateName}"
"TEMPLATE_SELECTED_SUBTITLE": "Şablonu yapılandır: {templateName}"
},
"PICKER": {
"SEARCH_PLACEHOLDER": "Şablon Ara",
"NO_TEMPLATES_FOUND": "İçin hiç şablon bulunamadı",
"NO_CONTENT": "No content",
"HEADER": "Header",
"BODY": "Body",
"FOOTER": "Footer",
"BUTTONS": "Buttons",
"HEADER": "Başlık",
"BODY": "Gövde",
"FOOTER": "Altbilgi",
"BUTTONS": "Butonlar",
"CATEGORY": "Kategori",
"MEDIA_CONTENT": "Media Content",
"MEDIA_CONTENT_FALLBACK": "media content",
"MEDIA_CONTENT": "Medya İçeriği",
"MEDIA_CONTENT_FALLBACK": "medya içeriği",
"NO_TEMPLATES_AVAILABLE": "No Twilio templates available. Click refresh to sync templates from Twilio.",
"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.",
"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.",
"LABELS": {
"LANGUAGE": "Dil",
"TEMPLATE_BODY": "Şablon İçeriği",
"CATEGORY": "Kategori"
},
"TYPES": {
"MEDIA": "Media",
"MEDIA": "Medya",
"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} Header",
"MEDIA_HEADER_LABEL": "{type} Başlık",
"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": "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.",
"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.",
"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": "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"
"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"
},
"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": "Open in new tab",
"COPY_LINK": "Copy conversation link",
"COPY_LINK_SUCCESS": "Conversation link copied to clipboard",
"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ı",
"API": {
"AGENT_ASSIGNMENT": {
"SUCCESFUL": "Sohbet kimliği {conversationId} \"{agentName}\" tarafından atanmış",
@@ -7,6 +7,6 @@
},
"CLOSE": "Kapat",
"BETA": "Beta",
"BETA_DESCRIPTION": "This feature is in beta and may change as we improve it."
"BETA_DESCRIPTION": "Bu özellik beta aşamasındadır ve geliştirdikçe değişiklik gösterebilir."
}
}
@@ -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": "You have exceeded the agent limit. Your plan only allows {allowedAgents} agents.",
"AGENTS": "Temsilci sınırını aştınız. Planınız yalnızca {allowedAgents} temsilciye izin veriyor.",
"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": "Enter a valid URL. The Home page link must start with 'http://' or 'https://'."
"ERROR": "Geçerli bir URL girin. Ana sayfa bağlantısı http:// veya https:// ile başlamalıdır."
},
"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": "Your custom portal will start working as soon as it is verified.",
"STATUS_DESCRIPTION": "Özel portalınız, doğrulandıktan sonra hemen çalışmaya başlayacaktır.",
"PLACEHOLDER": "Portal özel domain",
"EDIT_BUTTON": "Düzenle",
"ADD_BUTTON": "Add custom domain",
"STATUS": {
"LIVE": "Canlı",
"PENDING": "Awaiting verification",
"ERROR": "Verification failed"
"PENDING": "Doğrulama bekleniyor",
"ERROR": "Doğrulama başarısız"
},
"DIALOG": {
"ADD_HEADER": "Add custom domain",
@@ -771,14 +771,14 @@
"LABEL": "Özel domain",
"PLACEHOLDER": "Portal özel domain",
"ERROR": "Custom domain is required",
"FORMAT_ERROR": "Please enter a valid domain URL e.g. docs.yourdomain.com"
"FORMAT_ERROR": "Lütfen geçerli bir alan adı URL'si girin, örneğin 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": "Successfully copied CNAME",
"COPY": "CNAME başarıyla kopyalandı",
"SEND_INSTRUCTIONS": {
"HEADER": "Send instructions",
"HEADER": "Talimatları gönder",
"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": "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)",
"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ı)",
"ADDITIONAL_CONTEXT_PLACEHOLDER": "Provide any additional context or instructions for FAQ generation...",
"UPLOADING": "Yükleniyor ...",
"UPLOAD": "Upload & Process",
"UPLOAD": "Yükle & İşle",
"CANCEL": "İptal Et",
"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."
"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."
},
"PDF_DOCUMENTS": {
"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",
"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",
"GENERATE_FAQS": "Generate FAQs",
"GENERATING": "Oluşturuluyor...",
"CONFIRM_DELETE": "{filename} silmek istediğinizden emin misiniz?",
"EMPTY_STATE": {
"TITLE": "No PDF documents yet",
"TITLE": "Henüz PDF belgesi yok",
"DESCRIPTION": "Upload PDF documents to automatically generate FAQs using AI"
},
"STATUS": {
"UPLOADED": "Ready",
"PROCESSING": "Processing",
"UPLOADED": "Hazır",
"PROCESSING": "İşleniyor",
"PROCESSED": "Tamamlandı",
"FAILED": "Failed"
"FAILED": "Başarısız"
}
},
"CONTENT_GENERATION": {
"TITLE": "Content Generation",
"DESCRIPTION": "Upload PDF documents to automatically generate FAQ content using AI",
"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_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_SUCCESS": "Document processed successfully!",
"UPLOAD_ERROR": "Failed to upload document. Please try again.",
"INVALID_FILE_TYPE": "Please select a valid PDF file",
"FILE_TOO_LARGE": "File size must be less than 512MB",
"INVALID_FILE_TYPE": "Lütfen geçerli bir PDF dosyası seçin",
"FILE_TOO_LARGE": "Dosya boyutu 512 MB'tan az olmalıdır",
"GENERATED_CONTENT": "Generated FAQ Content",
"PUBLISH_SELECTED": "Publish Selected",
"PUBLISHING": "Publishing...",
"PUBLISHING": "Yayınlanıyor...",
"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": "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.",
"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.",
"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": "Quick setup through Meta",
"TWILIO_DESC": "Connect via Twilio credentials",
"WHATSAPP_CLOUD_DESC": "Meta üzerinden hızlı kurulum",
"TWILIO_DESC": "Twilio kimlik bilgileriyle bağlanın",
"360_DIALOG": "360Dialog"
},
"SELECT_PROVIDER": {
"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."
"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."
},
"INBOX_NAME": {
"LABEL": "Gelen Kutusu Adı",
@@ -272,69 +272,69 @@
},
"SUBMIT_BUTTON": "WhatsApp Kanalı Oluştur",
"EMBEDDED_SIGNUP": {
"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.",
"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.",
"BENEFITS": {
"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"
"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ı"
},
"LEARN_MORE": {
"TEXT": "To learn more about integrated signup, pricing, and limitations, visit {link}.",
"LINK_TEXT": "this link"
"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ı"
},
"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"
"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ışı"
},
"API": {
"ERROR_MESSAGE": "WhatsApp kanalını kaydedemedik"
}
},
"VOICE": {
"TITLE": "Voice Channel",
"DESC": "Integrate Twilio Voice and start supporting your customers via phone calls.",
"TITLE": "Ses Kanalı",
"DESC": "Twilio Voiceu entegre edin ve müşterilerinize telefon aramalarıyla destek vermeye başlayın.",
"PHONE_NUMBER": {
"LABEL": "Telefon Numarası",
"PLACEHOLDER": "Enter your phone number (e.g. +1234567890)",
"ERROR": "Please provide a valid phone number in E.164 format (e.g. +1234567890)"
"PLACEHOLDER": "Telefon numaranızı girin (örn. +901234567890)",
"ERROR": "Lütfen E.164 formatında geçerli bir telefon numarası girin (örn. +901234567890)"
},
"TWILIO": {
"ACCOUNT_SID": {
"LABEL": "Hesap SID'si",
"PLACEHOLDER": "Enter your Twilio Account SID",
"REQUIRED": "Account SID is required"
"PLACEHOLDER": "Twilio Account SIDinizi girin",
"REQUIRED": "Account SID gereklidir"
},
"AUTH_TOKEN": {
"LABEL": "Yetkilendirme Jetonu",
"PLACEHOLDER": "Enter your Twilio Auth Token",
"REQUIRED": "Auth Token is required"
"PLACEHOLDER": "Twilio Auth Tokeninizi girin",
"REQUIRED": "Auth Token gereklidir"
},
"API_KEY_SID": {
"LABEL": "API Anahtar SID",
"PLACEHOLDER": "Enter your Twilio API Key SID",
"REQUIRED": "API Key SID is required"
"PLACEHOLDER": "Twilio API Key SIDinizi girin",
"REQUIRED": "API Key SID gereklidir"
},
"API_KEY_SECRET": {
"LABEL": "API Anahtar Sırrı",
"PLACEHOLDER": "Enter your Twilio API Key Secret",
"REQUIRED": "API Key Secret is required"
"PLACEHOLDER": "Twilio API Key Secret’ınızı girin",
"REQUIRED": "API Key Secret gereklidir"
}
},
"CONFIGURATION": {
"TWILIO_VOICE_URL_TITLE": "Twilio Voice URL",
"TWILIO_VOICE_URL_TITLE": "Twilio Ses 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": "Complete the setup",
"TITLE_NEXT": "Kurulumu tamamlayın",
"TITLE_FINISH": "Voilà!",
"CHANNEL": {
"WEBSITE": {
"TITLE": "Website",
"DESCRIPTION": "Create a live-chat widget"
"DESCRIPTION": "Canlı sohbet widget'ı oluşturun"
},
"FACEBOOK": {
"TITLE": "Facebook\n",
@@ -466,7 +466,7 @@
"DESCRIPTION": "Connect your instagram account"
},
"VOICE": {
"TITLE": "Voice",
"TITLE": "Ses",
"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": "Allowed Domains",
"TITLE": "İzin Verilen Alan Adları",
"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": "Reconfigure",
"WHATSAPP_RECONFIGURE_BUTTON": "Yeniden yapılandır",
"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 App ID is not configured. Please contact your administrator.",
"WHATSAPP_CONFIG_ID_MISSING": "WhatsApp Configuration ID is not configured. Please contact your administrator.",
"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_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": "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_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_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": "Voice"
"VOICE": "Ses"
}
}
}
@@ -487,12 +487,12 @@
"ASSISTANT": "Assistant"
},
"BASIC_SETTINGS": {
"TITLE": "Basic settings",
"DESCRIPTION": "Customize what the assistant says when ending a conversation or transferring to a human."
"TITLE": "Temel ayarlar",
"DESCRIPTION": "Konuşmayı sonlandırırken veya bir operatöre aktarırken asistanın söyleyeceği sözleri özelleştirin."
},
"SYSTEM_SETTINGS": {
"TITLE": "System settings",
"DESCRIPTION": "Customize what the assistant says when ending a conversation or transferring to a human."
"TITLE": "Sistem ayarları",
"DESCRIPTION": "Konuşmayı sonlandırırken veya bir operatöre aktarırken asistanın söyleyeceği sözleri özelleştirin."
},
"CONTROL_ITEMS": {
"TITLE": "The Fun Stuff",
@@ -534,16 +534,16 @@
},
"BULK_ACTION": {
"SELECTED": "{count} item selected | {count} items selected",
"SELECT_ALL": "Select all ({count})",
"UNSELECT_ALL": "Unselect all ({count})",
"SELECT_ALL": "Tümünü seç ({count})",
"UNSELECT_ALL": "Tümünü kaldır ({count})",
"BULK_DELETE_BUTTON": "Sil"
},
"ADD": {
"SUGGESTED": {
"TITLE": "Example guardrails",
"ADD": "Add all",
"ADD_SINGLE": "Add this",
"SAVE": "Add and save (↵)",
"ADD": "Tümünü ekle",
"ADD_SINGLE": "Bunu ekle",
"SAVE": "Ekle ve kaydet (↵)",
"PLACEHOLDER": "Type in another guardrail..."
},
"NEW": {
@@ -551,11 +551,11 @@
"CREATE": "Yarat",
"CANCEL": "İptal Et",
"PLACEHOLDER": "Type in another guardrail...",
"TEST_ALL": "Test all"
"TEST_ALL": "Tümünü test et"
}
},
"LIST": {
"SEARCH_PLACEHOLDER": "Search..."
"SEARCH_PLACEHOLDER": "Ara..."
},
"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} item selected | {count} items selected",
"SELECT_ALL": "Select all ({count})",
"UNSELECT_ALL": "Unselect all ({count})",
"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})",
"BULK_DELETE_BUTTON": "Sil"
},
"ADD": {
"SUGGESTED": {
"TITLE": "Example response guidelines",
"ADD": "Add all",
"ADD_SINGLE": "Add this",
"SAVE": "Add and save (↵)",
"ADD": "Tümünü ekle",
"ADD_SINGLE": "Bunu ekle",
"SAVE": "Ekle ve kaydet (↵)",
"PLACEHOLDER": "Type in another response guideline..."
},
"NEW": {
@@ -599,11 +599,11 @@
"CREATE": "Yarat",
"CANCEL": "İptal Et",
"PLACEHOLDER": "Type in another response guideline...",
"TEST_ALL": "Test all"
"TEST_ALL": "Tümünü test et"
}
},
"LIST": {
"SEARCH_PLACEHOLDER": "Search..."
"SEARCH_PLACEHOLDER": "Ara..."
},
"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": "Select all ({count})",
"UNSELECT_ALL": "Unselect all ({count})",
"SELECT_ALL": "Tümünü seç ({count})",
"UNSELECT_ALL": "Tümünü kaldır ({count})",
"BULK_DELETE_BUTTON": "Sil"
},
"ADD": {
"SUGGESTED": {
"TITLE": "Example scenarios",
"ADD": "Add all",
"ADD_SINGLE": "Add this",
"ADD": "Tümünü ekle",
"ADD_SINGLE": "Bunu ekle",
"TOOLS_USED": "Tools used :"
},
"NEW": {
@@ -667,10 +667,10 @@
},
"UPDATE": {
"CANCEL": "İptal Et",
"UPDATE": "Update changes"
"UPDATE": "Değişiklikleri güncelle"
},
"LIST": {
"SEARCH_PLACEHOLDER": "Search..."
"SEARCH_PLACEHOLDER": "Ara..."
},
"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": "Document Type",
"LABEL": "Belge Türü",
"URL": "URL",
"PDF": "PDF File"
"PDF": "PDF Dosya"
},
"URL": {
"LABEL": "URL",
@@ -715,16 +715,16 @@
"ERROR": "Please provide a valid URL for the document"
},
"PDF_FILE": {
"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"
"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"
},
"NAME": {
"LABEL": "Document Name (Optional)",
"PLACEHOLDER": "Enter a name for the document"
"LABEL": "Belge Adı (İsteğe Bağlı)",
"PLACEHOLDER": "Belgeye bir ad girin"
},
"ASSISTANT": {
"LABEL": "Assistant",
@@ -759,9 +759,9 @@
"CONVERSATION": "Conversation #{id}"
},
"SELECTED": "{count} selected",
"SELECT_ALL": "Select all ({count})",
"UNSELECT_ALL": "Unselect all ({count})",
"SEARCH_PLACEHOLDER": "Search FAQs...",
"SELECT_ALL": "Tümünü seç ({count})",
"UNSELECT_ALL": "Tümünü kaldır ({count})",
"SEARCH_PLACEHOLDER": "Sıkça Sorulan Soruları Ara...",
"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": "Log in via SSO",
"LABEL": "SSO ile giriş yapın",
"TITLE": "Initiate Single Sign-on (SSO)",
"SUBTITLE": "Enter your work email to access your organization",
"BACK_TO_LOGIN": "Login via Password",
"BACK_TO_LOGIN": "Şifre ile giriş yap",
"WORK_EMAIL": {
"LABEL": "Work Email",
"PLACEHOLDER": "Enter your work email"
@@ -1,10 +1,10 @@
{
"MFA_SETTINGS": {
"TITLE": "Two-Factor Authentication",
"TITLE": "İki Faktörlü Kimlik Doğrulama",
"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": "Authentication Status",
"STATUS_DESCRIPTION": "Manage your two-factor authentication settings and backup recovery codes",
"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",
"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": "Can't scan? Enter code manually",
"SECRET_KEY": "Secret Key",
"MANUAL_ENTRY": "Tarama yapamıyor musunuz? Kodu manuel olarak girin",
"SECRET_KEY": "Gizli Anahtar",
"COPY": "Kopyala",
"ENTER_CODE": "Enter the 6-digit code from your authenticator app",
"ENTER_CODE": "Doğrulama uygulamanızdan 6 haneli kodu girin",
"ENTER_CODE_PLACEHOLDER": "000000",
"VERIFY_BUTTON": "Verify & Continue",
"VERIFY_BUTTON": "Doğrula & Devam Et",
"CANCEL": "İptal Et",
"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"
"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"
},
"BACKUP": {
"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.",
"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.",
"DOWNLOAD": "İndir",
"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"
"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ı"
},
"MANAGEMENT": {
"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"
"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"
},
"DISABLE": {
"TITLE": "Disable Two-Factor Authentication",
"DESCRIPTION": "You'll need to enter your password and a verification code to disable two-factor authentication.",
"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.",
"PASSWORD": "Parola",
"OTP_CODE": "Verification Code",
"OTP_CODE": "Doğrulama Kodu",
"OTP_CODE_PLACEHOLDER": "000000",
"CONFIRM": "Disable 2FA",
"CONFIRM": "2FA'yı devre dışı bırak",
"CANCEL": "İptal Et",
"SUCCESS": "Two-factor authentication has been disabled",
"ERROR": "Failed to disable MFA. Please check your credentials."
"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."
},
"REGENERATE": {
"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",
"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",
"OTP_CODE_PLACEHOLDER": "000000",
"CONFIRM": "Generate New Codes",
"CONFIRM": "Yeni Kodlar Oluştur",
"CANCEL": "İptal Et",
"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"
"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ı"
}
},
"MFA_VERIFICATION": {
"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",
"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",
"BACKUP_CODE_PLACEHOLDER": "000000",
"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",
"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",
"HELP_MODAL": {
"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."
"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."
},
"VERIFICATION_FAILED": "Verification failed. Please try again."
"VERIFICATION_FAILED": "Doğrulama başarısız oldu. Lütfen tekrar deneyin."
}
}
@@ -53,11 +53,11 @@
}
},
"LANGUAGE": {
"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"
"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"
}
},
"MESSAGE_SIGNATURE_SECTION": {
@@ -81,9 +81,9 @@
"BTN_TEXT": "Şifre değiştir"
},
"SECURITY_SECTION": {
"TITLE": "Security",
"NOTE": "Manage additional security features for your account.",
"MFA_BUTTON": "Manage Two-Factor Authentication"
"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"
},
"ACCESS_TOKEN": {
"TITLE": "Erişim Jetonu",
@@ -238,7 +238,7 @@
"APPEARANCE": "Change appearance",
"SUPER_ADMIN_CONSOLE": "SuperAdmin console",
"DOCS": "Read documentation",
"CHANGELOG": "Changelog",
"CHANGELOG": "Değişiklik Günlüğü",
"LOGOUT": "Log out"
},
"APP_GLOBAL": {
@@ -342,7 +342,7 @@
"REPORTS_LABEL": "Etiketler",
"REPORTS_INBOX": "Gelen kutusu",
"REPORTS_TEAM": "Ekip",
"AGENT_ASSIGNMENT": "Agent Assignment",
"AGENT_ASSIGNMENT": "Temsilci Atama",
"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": "Security"
"SECURITY": "Güvenlik"
},
"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": "Security",
"DESCRIPTION": "Manage your account security settings.",
"TITLE": "Güvenlik",
"DESCRIPTION": "Hesap güvenlik ayarlarınızı yönetin.",
"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": "Certificate is required",
"CERTIFICATE_ERROR": "Sertifika gereklidir",
"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": "Upgrade now",
"UPGRADE_NOW": "Şimdi yükselt",
"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": "Continue",
"CONFIRM_BUTTON_LABEL": "Devam et",
"CANCEL_BUTTON_LABEL": "İptal Et"
},
"API": {
@@ -620,7 +620,7 @@
},
"FAIR_DISTRIBUTION": {
"LABEL": "Fair distribution policy",
"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.",
"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.",
"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": "Continue",
"CONFIRM_BUTTON_LABEL": "Devam et",
"CANCEL_BUTTON_LABEL": "İptal Et"
},
"API": {
@@ -706,19 +706,19 @@
"ADD_BUTTON": "Add inbox",
"FIELD": {
"SELECT_INBOX": "Select inbox",
"MAX_CONVERSATIONS": "Max conversations",
"SET_LIMIT": "Set limit"
"MAX_CONVERSATIONS": "Maksimum Konuşma",
"SET_LIMIT": "Sınır belirle"
},
"EMPTY_STATE": "No inbox limit set"
"EMPTY_STATE": "Gelen kutusu sınırı belirlenmemiş"
},
"EXCLUSION_RULES": {
"LABEL": "Exclusion rules",
"LABEL": "Hariç tutma kuralları",
"DESCRIPTION": "Conversations that satisfy the following conditions would not count towards agent capacity",
"TAGS": {
"LABEL": "Exclude conversations tagged with specific labels",
"ADD_TAG": "add tag",
"ADD_TAG": "etiket ekle",
"DROPDOWN": {
"SEARCH_PLACEHOLDER": "Search and select tags to add"
"SEARCH_PLACEHOLDER": "Eklemek için etiketleri arayın ve seçin"
},
"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": "Configure template: {templateName}"
"TEMPLATE_SELECTED_SUBTITLE": "Şablonu yapılandır: {templateName}"
},
"PICKER": {
"SEARCH_PLACEHOLDER": "Şablon Ara",
"NO_TEMPLATES_FOUND": "İçin hiç şablon bulunamadı",
"HEADER": "Header",
"BODY": "Body",
"FOOTER": "Footer",
"BUTTONS": "Buttons",
"HEADER": "Başlık",
"BODY": "Gövde",
"FOOTER": "Altbilgi",
"BUTTONS": "Butonlar",
"CATEGORY": "Kategori",
"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.",
"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.",
"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} 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"
"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"
}
}
}
+35 -3
View File
@@ -1,8 +1,11 @@
<script setup>
import { defineProps, defineModel } from 'vue';
import { defineProps, defineModel, computed } from 'vue';
import { useToggle } from '@vueuse/core';
import Button from 'dashboard/components-next/button/Button.vue';
import WithLabel from './WithLabel.vue';
defineProps({
const props = defineProps({
label: {
type: String,
required: true,
@@ -31,6 +34,11 @@ defineProps({
},
});
const FIELDS = {
TEXT: 'text',
PASSWORD: 'password',
};
defineOptions({
inheritAttrs: false,
});
@@ -39,6 +47,17 @@ 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>
@@ -56,7 +75,7 @@ const model = defineModel({
v-bind="$attrs"
v-model="model"
:name="name"
:type="type"
:type="currentInputType"
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':
@@ -66,7 +85,20 @@ const model = defineModel({
'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 } from '@vuelidate/validators';
import { required, minLength, email, sameAs } from '@vuelidate/validators';
import { mapGetters } from 'vuex';
import { useAlert } from 'dashboard/composables';
import { DEFAULT_REDIRECT_URL } from 'dashboard/constants/globals';
@@ -8,17 +8,22 @@ 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() {
@@ -31,6 +36,7 @@ export default {
fullName: '',
email: '',
password: '',
confirmPassword: '',
hCaptchaClientResponse: '',
},
didCaptchaReset: false,
@@ -59,7 +65,12 @@ export default {
password: {
required,
isValidPassword,
minLength: minLength(6),
minLength: minLength(MIN_PASSWORD_LENGTH),
},
confirmPassword: {
required,
minLength: minLength(MIN_PASSWORD_LENGTH),
sameAsPassword: sameAs(this.credentials.password),
},
},
};
@@ -80,16 +91,11 @@ export default {
}
return true;
},
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');
confirmPasswordErrorText() {
const { confirmPassword } = this.v$.credentials;
if (!confirmPassword.$error) return '';
if (confirmPassword.sameAsPassword.$invalid) {
return this.$t('REGISTER.CONFIRM_PASSWORD.ERROR');
}
return '';
},
@@ -99,6 +105,51 @@ 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() {
@@ -126,9 +177,7 @@ 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;
@@ -183,9 +232,42 @@ export default {
:label="$t('LOGIN.PASSWORD.LABEL')"
:placeholder="$t('SET_NEW_PASSWORD.PASSWORD.PLACEHOLDER')"
:has-error="v$.credentials.password.$error"
:error-message="passwordErrorText"
aria-describedby="password-requirements"
@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": "We will be back as soon as possible"
"BACK_AS_SOON_AS_POSSIBLE": "Wrócimy tak szybko, jak to możliwe"
},
"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": "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"
"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"
},
"DAY_NAMES": {
"SUNDAY": "Niedziela",
+8 -8
View File
@@ -14,24 +14,24 @@
},
"THUMBNAIL": {
"AUTHOR": {
"NOT_AVAILABLE": "Not available"
"NOT_AVAILABLE": "Şu anda müsait değil"
}
},
"TEAM_AVAILABILITY": {
"ONLINE": "Çevrimiçi",
"OFFLINE": "Şu an operatörlerimiz müsait değil",
"BACK_AS_SOON_AS_POSSIBLE": "We will be back as soon as possible"
"BACK_AS_SOON_AS_POSSIBLE": "En kısa sürede geri döneceğiz"
},
"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": "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"
"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"
},
"DAY_NAMES": {
"SUNDAY": "Pazar",
@@ -44,13 +44,6 @@ 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
@@ -60,10 +53,8 @@ class Twilio::IncomingMessageService
end
def set_contact
source_id = twilio_channel.whatsapp? ? normalized_phone_number : params[:From]
contact_inbox = ::ContactInboxWithContactBuilder.new(
source_id: source_id,
source_id: params[:From],
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_by_provider(waid, :cloud)
Whatsapp::PhoneNumberNormalizationService.new(inbox).normalize_and_find_contact(waid)
end
def error_webhook_event?(message)
@@ -1,13 +0,0 @@
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,40 +1,23 @@
# Service to handle phone number normalization for WhatsApp messages
# Currently supports Brazil and Argentina phone number format variations
# Supports both WhatsApp Cloud API and Twilio WhatsApp providers
# Currently supports Brazil phone number format variations
# Designed to be extensible for additional countries in future PRs
#
# 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)
# Usage: Whatsapp::PhoneNumberNormalizationService.new(inbox).normalize_and_find_contact(waid)
class Whatsapp::PhoneNumberNormalizationService
def initialize(inbox)
@inbox = inbox
end
# 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)
# 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
# Find appropriate normalizer for the country
normalizer = find_normalizer_for_country(clean_number)
return raw_number unless normalizer
normalized_waid = normalizer.normalize(waid)
existing_contact_inbox = find_existing_contact_inbox(normalized_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
existing_contact_inbox&.source_id || waid
end
private
@@ -50,32 +33,7 @@ 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::ArgentinaPhoneNormalizer
Whatsapp::PhoneNormalizers::BrazilPhoneNormalizer
].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: true
json.partial! 'api/v1/models/contact', formats: [:json], resource: contact, with_contact_inboxes: @include_contact_inboxes
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: 'Authentication failed. Please check your credentials and try again.'
authentication_failed: 'Kimlik doğrulama başarısız oldu. Lütfen kimlik bilgilerinizi kontrol edin ve tekrar deneyin.'
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 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.
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.
profile:
mfa:
enabled: MFA enabled successfully
disabled: MFA disabled successfully
enabled: MFA başarıyla etkinleştirildi
disabled: MFA başarıyla devre dışı bırakıldı
account_saml_settings:
invalid_certificate: must be a valid X.509 certificate in PEM format
invalid_certificate: PEM formatında geçerli bir X.509 sertifikası olmalıdır
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ünürlük Sayısı
resolution_count: Çözüm 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: 'Auto-resolve message not sent due to outgoing message restrictions'
not_sent_due_to_messaging_window: 'Giden mesaj kısıtlamaları nedeniyle otomatik çözüm mesajı gönderilmedi'
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: '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}'
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}'
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 Error: %{error}'
openai_api_error: 'OpenAI API Hatası: %{error}'
starting_paginated_faq: 'Starting paginated FAQ generation (%{pages_per_chunk} pages per chunk)'
stopping_faq_generation: 'Stopping processing. Reason: %{reason}'
stopping_faq_generation: 'İşlem durduruldu. Sebep: %{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: 'Invalid email format'
custom_domain_not_configured: 'Custom domain is not configured'
invalid_email_format: 'Geçersiz e-posta formatı'
custom_domain_not_configured: 'Özel alan adı yapılandırılmadı'
instructions_sent_successfully: 'Instructions sent successfully'
subject: 'Finish setting up %{custom_domain}'
subject: '%{custom_domain} kurulumunu tamamlayın'
ssl_status:
custom_domain_not_configured: 'Özel alan adı yapılandırılmamış'
+21 -3
View File
@@ -24,13 +24,31 @@ class Integrations::Slack::ChannelBuilder
end
def channels
conversations_list = slack_client.conversations_list(types: 'public_channel,private_channel', exclude_archived: true)
# 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)
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: 'public_channel,private_channel',
exclude_archived: true
types: channel_type,
exclude_archived: true,
limit: limit
)
channel_list.concat(conversations_list.channels)
end
-4
View File
@@ -13,9 +13,5 @@ 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.user = nil
Current.reset
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,230 +402,6 @@ 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
@@ -1,95 +0,0 @@
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