Merge branch 'develop' into chore/load-reply-message

This commit is contained in:
Sivin Varghese
2025-11-13 13:36:25 +05:30
committed by GitHub
141 changed files with 4639 additions and 937 deletions
+32 -43
View File
@@ -1,5 +1,8 @@
class Messages::MessageBuilder
include ::FileTypeHelper
include ::EmailHelper
include ::DataHelper
attr_reader :message
def initialize(user, conversation, params)
@@ -38,30 +41,12 @@ class Messages::MessageBuilder
params = convert_to_hash(@params)
content_attributes = params.fetch(:content_attributes, {})
return parse_json(content_attributes) if content_attributes.is_a?(String)
return safe_parse_json(content_attributes) if content_attributes.is_a?(String)
return content_attributes if content_attributes.is_a?(Hash)
{}
end
# Converts the given object to a hash.
# If it's an instance of ActionController::Parameters, converts it to an unsafe hash.
# Otherwise, returns the object as-is.
def convert_to_hash(obj)
return obj.to_unsafe_h if obj.instance_of?(ActionController::Parameters)
obj
end
# Attempts to parse a string as JSON.
# If successful, returns the parsed hash with symbolized names.
# If unsuccessful, returns nil.
def parse_json(content)
JSON.parse(content, symbolize_names: true)
rescue JSON::ParserError
{}
end
def process_attachments
return if @attachments.blank?
@@ -110,12 +95,6 @@ class Messages::MessageBuilder
email_string.gsub(/\s+/, '').split(',')
end
def validate_email_addresses(all_emails)
all_emails&.each do |email|
raise StandardError, 'Invalid email address' unless email.match?(URI::MailTo::EMAIL_REGEXP)
end
end
def message_type
if @conversation.inbox.channel_type != 'Channel::Api' && @message_type == 'incoming'
raise StandardError, 'Incoming messages are only allowed in Api inboxes'
@@ -178,14 +157,17 @@ class Messages::MessageBuilder
email_attributes = ensure_indifferent_access(@message.content_attributes[:email] || {})
normalized_content = normalize_email_body(@message.content)
# Process liquid templates in normalized content with code block protection
processed_content = process_liquid_in_email_body(normalized_content)
# Use custom HTML content if provided, otherwise generate from message content
email_attributes[:html_content] = if custom_email_content_provided?
build_custom_html_content
else
build_html_content(normalized_content)
build_html_content(processed_content)
end
email_attributes[:text_content] = build_text_content(normalized_content)
email_attributes[:text_content] = build_text_content(processed_content)
email_attributes
end
@@ -204,22 +186,6 @@ class Messages::MessageBuilder
text_content
end
def ensure_indifferent_access(hash)
return {} if hash.blank?
hash.respond_to?(:with_indifferent_access) ? hash.with_indifferent_access : hash
end
def normalize_email_body(content)
content.to_s.gsub("\r\n", "\n")
end
def render_email_html(content)
return '' if content.blank?
ChatwootMarkdownRenderer.new(content).render_message.to_s
end
def custom_email_content_provided?
@params[:email_html_content].present?
end
@@ -232,4 +198,27 @@ class Messages::MessageBuilder
html_content
end
# Liquid processing methods for email content
def process_liquid_in_email_body(content)
return content if content.blank?
return content unless should_process_liquid?
# Protect code blocks from liquid processing
modified_content = modified_liquid_content(content)
template = Liquid::Template.parse(modified_content)
template.render(drops_with_sender)
rescue Liquid::Error
content
end
def should_process_liquid?
@message_type == 'outgoing' || @message_type == 'template'
end
def drops_with_sender
message_drops(@conversation).merge({
'agent' => UserDrop.new(sender)
})
end
end
@@ -22,9 +22,10 @@ class Api::V1::Accounts::ArticlesController < Api::V1::Accounts::BaseController
def edit; end
def create
@article = @portal.articles.create!(article_params)
params_with_defaults = article_params
params_with_defaults[:status] ||= :draft
@article = @portal.articles.create!(params_with_defaults)
@article.associate_root_article(article_params[:associated_article_id])
@article.draft!
render json: { error: @article.errors.messages }, status: :unprocessable_entity and return unless @article.valid?
end
@@ -23,7 +23,7 @@ class Api::V1::Accounts::WebhooksController < Api::V1::Accounts::BaseController
private
def webhook_params
params.require(:webhook).permit(:inbox_id, :url, subscriptions: [])
params.require(:webhook).permit(:inbox_id, :name, :url, subscriptions: [])
end
def fetch_webhook
+24
View File
@@ -0,0 +1,24 @@
# Provides utility methods for data transformation, hash manipulation, and JSON parsing.
# This module contains helper methods for converting between different data types,
# normalizing hashes, and safely handling JSON operations.
module DataHelper
# Ensures a hash supports indifferent access (string or symbol keys).
# Returns an empty hash if the input is blank.
def ensure_indifferent_access(hash)
return {} if hash.blank?
hash.respond_to?(:with_indifferent_access) ? hash.with_indifferent_access : hash
end
def convert_to_hash(obj)
return obj.to_unsafe_h if obj.instance_of?(ActionController::Parameters)
obj
end
def safe_parse_json(content)
JSON.parse(content, symbolize_names: true)
rescue JSON::ParserError
{}
end
end
+21 -1
View File
@@ -4,6 +4,19 @@ module EmailHelper
domain.split('.').first
end
def render_email_html(content)
return '' if content.blank?
ChatwootMarkdownRenderer.new(content).render_message.to_s
end
# Raise a standard error if any email address is invalid
def validate_email_addresses(emails_to_test)
emails_to_test&.each do |email|
raise StandardError, 'Invalid email address' unless email.match?(URI::MailTo::EMAIL_REGEXP)
end
end
# ref: https://www.rfc-editor.org/rfc/rfc5233.html
# This is not a mandatory requirement for email addresses, but it is a common practice.
# john+test@xyc.com is the same as john@xyc.com
@@ -21,6 +34,10 @@ module EmailHelper
end
end
def normalize_email_body(content)
content.to_s.gsub("\r\n", "\n")
end
def modified_liquid_content(email)
# This regex is used to match the code blocks in the content
# We don't want to process liquid in code blocks
@@ -29,7 +46,10 @@ module EmailHelper
def message_drops(conversation)
{
'contact' => ContactDrop.new(conversation.contact)
'contact' => ContactDrop.new(conversation.contact),
'conversation' => ConversationDrop.new(conversation),
'inbox' => InboxDrop.new(conversation.inbox),
'account' => AccountDrop.new(conversation.account)
}
end
end
@@ -4,7 +4,6 @@ import { OnClickOutside } from '@vueuse/components';
import { useRoute } from 'vue-router';
import { useMapGetter } from 'dashboard/composables/store.js';
import { usePolicy } from 'dashboard/composables/usePolicy';
import Icon from 'dashboard/components-next/icon/Icon.vue';
import Button from 'dashboard/components-next/button/Button.vue';
import BackButton from 'dashboard/components/widgets/BackButton.vue';
import PaginationFooter from 'dashboard/components-next/pagination/PaginationFooter.vue';
@@ -117,58 +116,57 @@ const handleCreateAssistant = () => {
<div
class="flex items-start lg:items-center justify-between w-full py-6 lg:py-0 lg:h-20 gap-4 lg:gap-2 flex-col lg:flex-row"
>
<div class="flex gap-4 items-center">
<div class="flex gap-3 items-center">
<BackButton v-if="backUrl" :back-url="backUrl" />
<slot name="headerTitle">
<div v-if="showAssistantSwitcher" class="flex items-center gap-2">
<div class="flex items-center gap-1">
<span
v-if="activeAssistantName"
class="text-xl font-medium truncate text-n-slate-12"
<div v-if="showAssistantSwitcher" class="flex items-center gap-2">
<div class="flex items-center gap-2">
<span
v-if="activeAssistantName"
class="text-xl font-medium truncate text-n-slate-12"
>
{{ activeAssistantName }}
</span>
<div v-if="activeAssistantName" class="relative group">
<OnClickOutside
@trigger="showAssistantSwitcherDropdown = false"
>
{{ activeAssistantName }}
</span>
<div v-if="activeAssistantName" class="relative group">
<OnClickOutside
@trigger="showAssistantSwitcherDropdown = false"
>
<Button
icon="i-lucide-chevron-down"
variant="ghost"
color="slate"
size="xs"
class="rounded-md group-hover:bg-n-slate-3 hover:bg-n-slate-3 [&>span]:size-4"
@click="toggleAssistantSwitcher"
/>
<Button
icon="i-lucide-chevron-down"
variant="ghost"
color="slate"
size="xs"
class="rounded-md group-hover:bg-n-slate-3 hover:bg-n-slate-3 [&>span]:size-4"
@click="toggleAssistantSwitcher"
/>
<AssistantSwitcher
v-if="showAssistantSwitcherDropdown"
class="absolute ltr:left-0 rtl:right-0 top-9"
@close="showAssistantSwitcherDropdown = false"
@create-assistant="handleCreateAssistant"
/>
</OnClickOutside>
</div>
<Icon
v-if="activeAssistantName"
icon="i-lucide-chevron-right"
class="size-6 text-n-slate-11"
/>
<span class="text-xl font-medium text-n-slate-11">
{{ headerTitle }}
</span>
<AssistantSwitcher
v-if="showAssistantSwitcherDropdown"
class="absolute ltr:left-0 rtl:right-0 top-9"
@close="showAssistantSwitcherDropdown = false"
@create-assistant="handleCreateAssistant"
/>
</OnClickOutside>
</div>
</div>
<span v-else class="text-xl font-medium text-n-slate-12">
</div>
<div class="flex items-center gap-4">
<div
v-if="showAssistantSwitcher && headerTitle"
class="w-0.5 h-4 rounded-2xl bg-n-weak"
/>
<span
v-if="headerTitle"
class="text-xl font-medium text-n-slate-12"
>
{{ headerTitle }}
</span>
</slot>
<div
v-if="!isEmpty && showKnowMore"
class="flex items-center gap-2"
>
<div class="w-0.5 h-4 rounded-2xl bg-n-weak" />
<slot name="knowMore" />
<div
v-if="!isEmpty && showKnowMore"
class="flex items-center gap-2"
>
<div class="w-0.5 h-4 rounded-2xl bg-n-weak" />
<slot name="knowMore" />
</div>
</div>
</div>
@@ -75,9 +75,9 @@ const sendMessage = async () => {
<template>
<div
class="flex flex-col h-full rounded-lg p-4 border border-n-slate-4 text-n-slate-11"
class="flex flex-col h-full rounded-xl border py-6 border-n-weak text-n-slate-11"
>
<div class="mb-4">
<div class="mb-8 px-6">
<div class="flex justify-between items-center mb-1">
<h3 class="text-lg font-medium">
{{ t('CAPTAIN.PLAYGROUND.HEADER') }}
@@ -85,6 +85,7 @@ const sendMessage = async () => {
<NextButton
ghost
sm
slate
icon="i-lucide-rotate-ccw"
@click="resetConversation"
/>
@@ -97,11 +98,11 @@ const sendMessage = async () => {
<MessageList :messages="messages" :is-loading="isLoading" />
<div
class="flex items-center bg-n-solid-1 outline outline-n-container rounded-lg p-3"
class="flex items-center mx-6 bg-n-background outline outline-1 outline-n-weak rounded-xl p-3"
>
<input
v-model="newMessage"
class="flex-1 bg-transparent border-none focus:outline-none text-sm mb-0"
class="flex-1 bg-transparent border-none focus:outline-none text-sm mb-0 text-n-slate-12 placeholder:text-n-slate-10"
:placeholder="t('CAPTAIN.PLAYGROUND.MESSAGE_PLACEHOLDER')"
@keyup.enter="sendMessage"
/>
@@ -35,8 +35,8 @@ const getAvatarName = sender =>
const getMessageStyle = sender =>
isUserMessage(sender)
? 'bg-n-strong text-n-white'
: 'bg-n-solid-iris text-n-slate-12';
? 'bg-n-solid-blue text-n-slate-12 rounded-br-sm rounded-bl-xl rounded-t-xl'
: 'bg-n-solid-iris text-n-slate-12 rounded-bl-sm rounded-br-xl rounded-t-xl';
const scrollToBottom = async () => {
await nextTick();
@@ -49,7 +49,10 @@ watch(() => props.messages.length, scrollToBottom);
</script>
<template>
<div ref="messageContainer" class="flex-1 overflow-y-auto mb-4 space-y-2">
<div
ref="messageContainer"
class="flex-1 overflow-y-auto mb-4 px-6 space-y-6"
>
<div
v-for="(message, index) in messages"
:key="index"
@@ -57,15 +60,20 @@ watch(() => props.messages.length, scrollToBottom);
:class="getMessageAlignment(message.sender)"
>
<div
class="flex items-start gap-1.5"
class="flex items-end gap-1.5 max-w-[90%] md:max-w-[60%]"
:class="getMessageDirection(message.sender)"
>
<Avatar :name="getAvatarName(message.sender)" rounded-full :size="24" />
<Avatar
:name="getAvatarName(message.sender)"
rounded-full
:size="24"
class="shrink-0"
/>
<div
class="max-w-[80%] rounded-lg p-3 text-sm"
class="px-4 py-3 text-sm [overflow-wrap:break-word]"
:class="getMessageStyle(message.sender)"
>
<div class="break-words" v-html="formatMessage(message.content)" />
<div v-html="formatMessage(message.content)" />
</div>
</div>
</div>
@@ -8,11 +8,15 @@ import Avatar from 'dashboard/components-next/avatar/Avatar.vue';
const props = defineProps({
menuItems: {
type: Array,
required: true,
default: () => [],
validator: value => {
return value.every(item => item.action && item.value && item.label);
},
},
menuSections: {
type: Array,
default: () => [],
},
thumbnailSize: {
type: Number,
default: 20,
@@ -42,19 +46,62 @@ const { t } = useI18n();
const searchInput = ref(null);
const searchQuery = ref('');
const filteredMenuItems = computed(() => {
if (!searchQuery.value) return props.menuItems;
const hasSections = computed(() => props.menuSections.length > 0);
return props.menuItems.filter(item =>
const flattenedMenuItems = computed(() => {
if (!hasSections.value) {
return props.menuItems;
}
return props.menuSections.flatMap(section => section.items || []);
});
const filteredMenuItems = computed(() => {
if (!searchQuery.value) return flattenedMenuItems.value;
return flattenedMenuItems.value.filter(item =>
item.label.toLowerCase().includes(searchQuery.value.toLowerCase())
);
});
const filteredMenuSections = computed(() => {
if (!hasSections.value) {
return [];
}
if (!searchQuery.value) {
return props.menuSections;
}
const query = searchQuery.value.toLowerCase();
return props.menuSections
.map(section => {
const filteredItems = (section.items || []).filter(item =>
item.label.toLowerCase().includes(query)
);
return {
...section,
items: filteredItems,
};
})
.filter(section => section.items.length > 0);
});
const handleAction = item => {
const { action, value, ...rest } = item;
emit('action', { action, value, ...rest });
};
const shouldShowEmptyState = computed(() => {
if (hasSections.value) {
return filteredMenuSections.value.length === 0;
}
return filteredMenuItems.value.length === 0;
});
onMounted(() => {
if (searchInput.value && props.showSearch) {
searchInput.value.focus();
@@ -64,54 +111,122 @@ onMounted(() => {
<template>
<div
class="bg-n-alpha-3 backdrop-blur-[100px] border-0 outline outline-1 outline-n-container absolute rounded-xl z-50 py-2 px-2 gap-2 flex flex-col min-w-[136px] shadow-lg"
class="bg-n-alpha-3 backdrop-blur-[100px] border-0 outline outline-1 outline-n-container absolute rounded-xl z-50 gap-2 flex flex-col min-w-[136px] shadow-lg pb-2 px-2"
:class="{
'pt-2': !showSearch,
}"
>
<div v-if="showSearch" class="relative">
<span class="absolute i-lucide-search size-3.5 top-2 left-3" />
<input
ref="searchInput"
v-model="searchQuery"
type="search"
:placeholder="
searchPlaceholder || t('DROPDOWN_MENU.SEARCH_PLACEHOLDER')
"
class="reset-base w-full h-8 py-2 pl-10 pr-2 text-sm focus:outline-none border-none rounded-lg bg-n-alpha-black2 dark:bg-n-solid-1 text-n-slate-12"
/>
</div>
<button
v-for="(item, index) in filteredMenuItems"
:key="index"
type="button"
class="inline-flex items-center justify-start w-full h-8 min-w-0 gap-2 px-2 py-1.5 transition-all duration-200 ease-in-out border-0 rounded-lg z-60 hover:bg-n-alpha-1 dark:hover:bg-n-alpha-2 disabled:cursor-not-allowed disabled:pointer-events-none disabled:opacity-50"
:class="{
'bg-n-alpha-1 dark:bg-n-solid-active': item.isSelected,
'text-n-ruby-11': item.action === 'delete',
'text-n-slate-12': item.action !== 'delete',
}"
:disabled="item.disabled"
@click="handleAction(item)"
>
<slot name="thumbnail" :item="item">
<Avatar
v-if="item.thumbnail"
:name="item.thumbnail.name"
:src="item.thumbnail.src"
:size="thumbnailSize"
rounded-full
/>
</slot>
<Icon v-if="item.icon" :icon="item.icon" class="flex-shrink-0 size-3.5" />
<span v-if="item.emoji" class="flex-shrink-0">{{ item.emoji }}</span>
<span
v-if="item.label"
class="min-w-0 text-sm truncate"
:class="labelClass"
>
{{ item.label }}
</span>
</button>
<div
v-if="filteredMenuItems.length === 0"
v-if="showSearch"
class="sticky top-0 bg-n-alpha-3 backdrop-blur-sm pt-2"
>
<div class="relative">
<span class="absolute i-lucide-search size-3.5 top-2 left-3" />
<input
ref="searchInput"
v-model="searchQuery"
type="search"
:placeholder="
searchPlaceholder || t('DROPDOWN_MENU.SEARCH_PLACEHOLDER')
"
class="reset-base w-full h-8 py-2 pl-10 pr-2 text-sm focus:outline-none border-none rounded-lg bg-n-alpha-black2 dark:bg-n-solid-1 text-n-slate-12"
/>
</div>
</div>
<template v-if="hasSections">
<div
v-for="(section, sectionIndex) in filteredMenuSections"
:key="section.title || sectionIndex"
class="flex flex-col gap-1"
>
<p
v-if="section.title"
class="px-2 pt-2 text-xs font-medium text-n-slate-11 uppercase tracking-wide"
>
{{ section.title }}
</p>
<button
v-for="(item, itemIndex) in section.items"
:key="item.value || itemIndex"
type="button"
class="inline-flex items-center justify-start w-full h-8 min-w-0 gap-2 px-2 py-1.5 transition-all duration-200 ease-in-out border-0 rounded-lg z-60 hover:bg-n-alpha-1 dark:hover:bg-n-alpha-2 disabled:cursor-not-allowed disabled:pointer-events-none disabled:opacity-50"
:class="{
'bg-n-alpha-1 dark:bg-n-solid-active': item.isSelected,
'text-n-ruby-11': item.action === 'delete',
'text-n-slate-12': item.action !== 'delete',
}"
:disabled="item.disabled"
@click="handleAction(item)"
>
<slot name="thumbnail" :item="item">
<Avatar
v-if="item.thumbnail"
:name="item.thumbnail.name"
:src="item.thumbnail.src"
:size="thumbnailSize"
rounded-full
/>
</slot>
<Icon
v-if="item.icon"
:icon="item.icon"
class="flex-shrink-0 size-3.5"
/>
<span v-if="item.emoji" class="flex-shrink-0">{{ item.emoji }}</span>
<span
v-if="item.label"
class="min-w-0 text-sm truncate"
:class="labelClass"
>
{{ item.label }}
</span>
</button>
<div
v-if="sectionIndex < filteredMenuSections.length - 1"
class="h-px bg-n-alpha-2 mx-2 my-1"
/>
</div>
</template>
<template v-else>
<button
v-for="(item, index) in filteredMenuItems"
:key="index"
type="button"
class="inline-flex items-center justify-start w-full h-8 min-w-0 gap-2 px-2 py-1.5 transition-all duration-200 ease-in-out border-0 rounded-lg z-60 hover:bg-n-alpha-1 dark:hover:bg-n-alpha-2 disabled:cursor-not-allowed disabled:pointer-events-none disabled:opacity-50"
:class="{
'bg-n-alpha-1 dark:bg-n-solid-active': item.isSelected,
'text-n-ruby-11': item.action === 'delete',
'text-n-slate-12': item.action !== 'delete',
}"
:disabled="item.disabled"
@click="handleAction(item)"
>
<slot name="thumbnail" :item="item">
<Avatar
v-if="item.thumbnail"
:name="item.thumbnail.name"
:src="item.thumbnail.src"
:size="thumbnailSize"
rounded-full
/>
</slot>
<Icon
v-if="item.icon"
:icon="item.icon"
class="flex-shrink-0 size-3.5"
/>
<span v-if="item.emoji" class="flex-shrink-0">{{ item.emoji }}</span>
<span
v-if="item.label"
class="min-w-0 text-sm truncate"
:class="labelClass"
>
{{ item.label }}
</span>
</button>
</template>
<div
v-if="shouldShowEmptyState"
class="text-sm text-n-slate-11 px-2 py-1.5"
>
{{
@@ -14,7 +14,7 @@ import {
} from 'dashboard/constants/automation';
/**
* This is a shared composables that holds utilites used to build dropdown and file options
* This is a shared composables that holds utilities used to build dropdown and file options
* @returns {Object} An object containing various automation-related functions and computed properties.
*/
export default function useAutomationValues() {
@@ -21,7 +21,7 @@ export const initializeAudioAlerts = user => {
enable_audio_alerts: audioAlertType,
alert_if_unread_assigned_conversation_exist: alertIfUnreadConversationExist,
notification_tone: audioAlertTone,
// UI Settings can be undefined initally as we don't send the
// UI Settings can be undefined initially as we don't send the
// entire payload for the user during the signup process.
} = uiSettings || {};
@@ -579,7 +579,18 @@
"NO_LABELS_FOUND": "No labels available yet.",
"SELECTED_COUNT": "{count} selected",
"CLEAR_SELECTION": "Clear selection",
"SELECT_ALL": "Select all ({count})"
"SELECT_ALL": "Select all ({count})",
"DELETE_CONTACTS": "Delete",
"DELETE_SUCCESS": "Contacts deleted successfully.",
"DELETE_FAILED": "Failed to delete contacts.",
"DELETE_DIALOG": {
"TITLE": "Delete selected contacts",
"SINGULAR_TITLE": "Delete selected contact",
"DESCRIPTION": "This will permanently delete {count} selected contacts. This action cannot be undone.",
"SINGULAR_DESCRIPTION": "This will permanently delete the selected contact. This action cannot be undone.",
"CONFIRM_MULTIPLE": "Delete contacts",
"CONFIRM_SINGLE": "Delete contact"
}
},
"COMPOSE_NEW_CONVERSATION": {
"CONTACT_SEARCH": {
@@ -579,7 +579,18 @@
"NO_LABELS_FOUND": "No labels available yet.",
"SELECTED_COUNT": "{count} selected",
"CLEAR_SELECTION": "Clear selection",
"SELECT_ALL": "Select all ({count})"
"SELECT_ALL": "Select all ({count})",
"DELETE_CONTACTS": "حذف",
"DELETE_SUCCESS": "Contacts deleted successfully.",
"DELETE_FAILED": "Failed to delete contacts.",
"DELETE_DIALOG": {
"TITLE": "Delete selected contacts",
"SINGULAR_TITLE": "Delete selected contact",
"DESCRIPTION": "This will permanently delete {count} selected contacts. This action cannot be undone.",
"SINGULAR_DESCRIPTION": "This will permanently delete the selected contact. This action cannot be undone.",
"CONFIRM_MULTIPLE": "Delete contacts",
"CONFIRM_SINGLE": "حذف جهة الاتصال"
}
},
"COMPOSE_NEW_CONVERSATION": {
"CONTACT_SEARCH": {
@@ -579,7 +579,18 @@
"NO_LABELS_FOUND": "No labels available yet.",
"SELECTED_COUNT": "{count} selected",
"CLEAR_SELECTION": "Clear selection",
"SELECT_ALL": "Select all ({count})"
"SELECT_ALL": "Select all ({count})",
"DELETE_CONTACTS": "Delete",
"DELETE_SUCCESS": "Contacts deleted successfully.",
"DELETE_FAILED": "Failed to delete contacts.",
"DELETE_DIALOG": {
"TITLE": "Delete selected contacts",
"SINGULAR_TITLE": "Delete selected contact",
"DESCRIPTION": "This will permanently delete {count} selected contacts. This action cannot be undone.",
"SINGULAR_DESCRIPTION": "This will permanently delete the selected contact. This action cannot be undone.",
"CONFIRM_MULTIPLE": "Delete contacts",
"CONFIRM_SINGLE": "Delete contact"
}
},
"COMPOSE_NEW_CONVERSATION": {
"CONTACT_SEARCH": {
@@ -579,7 +579,18 @@
"NO_LABELS_FOUND": "No labels available yet.",
"SELECTED_COUNT": "{count} selected",
"CLEAR_SELECTION": "Clear selection",
"SELECT_ALL": "Select all ({count})"
"SELECT_ALL": "Select all ({count})",
"DELETE_CONTACTS": "Изтрий",
"DELETE_SUCCESS": "Contacts deleted successfully.",
"DELETE_FAILED": "Failed to delete contacts.",
"DELETE_DIALOG": {
"TITLE": "Delete selected contacts",
"SINGULAR_TITLE": "Delete selected contact",
"DESCRIPTION": "This will permanently delete {count} selected contacts. This action cannot be undone.",
"SINGULAR_DESCRIPTION": "This will permanently delete the selected contact. This action cannot be undone.",
"CONFIRM_MULTIPLE": "Delete contacts",
"CONFIRM_SINGLE": "Изтриване на контакта"
}
},
"COMPOSE_NEW_CONVERSATION": {
"CONTACT_SEARCH": {
@@ -579,7 +579,18 @@
"NO_LABELS_FOUND": "No labels available yet.",
"SELECTED_COUNT": "{count} selected",
"CLEAR_SELECTION": "Clear selection",
"SELECT_ALL": "Select all ({count})"
"SELECT_ALL": "Select all ({count})",
"DELETE_CONTACTS": "Delete",
"DELETE_SUCCESS": "Contacts deleted successfully.",
"DELETE_FAILED": "Failed to delete contacts.",
"DELETE_DIALOG": {
"TITLE": "Delete selected contacts",
"SINGULAR_TITLE": "Delete selected contact",
"DESCRIPTION": "This will permanently delete {count} selected contacts. This action cannot be undone.",
"SINGULAR_DESCRIPTION": "This will permanently delete the selected contact. This action cannot be undone.",
"CONFIRM_MULTIPLE": "Delete contacts",
"CONFIRM_SINGLE": "Delete contact"
}
},
"COMPOSE_NEW_CONVERSATION": {
"CONTACT_SEARCH": {
@@ -579,7 +579,18 @@
"NO_LABELS_FOUND": "No labels available yet.",
"SELECTED_COUNT": "{count} selected",
"CLEAR_SELECTION": "Clear selection",
"SELECT_ALL": "Select all ({count})"
"SELECT_ALL": "Select all ({count})",
"DELETE_CONTACTS": "Esborrar",
"DELETE_SUCCESS": "Contacts deleted successfully.",
"DELETE_FAILED": "Failed to delete contacts.",
"DELETE_DIALOG": {
"TITLE": "Delete selected contacts",
"SINGULAR_TITLE": "Delete selected contact",
"DESCRIPTION": "This will permanently delete {count} selected contacts. This action cannot be undone.",
"SINGULAR_DESCRIPTION": "This will permanently delete the selected contact. This action cannot be undone.",
"CONFIRM_MULTIPLE": "Delete contacts",
"CONFIRM_SINGLE": "Contacte esborrat"
}
},
"COMPOSE_NEW_CONVERSATION": {
"CONTACT_SEARCH": {
@@ -579,7 +579,18 @@
"NO_LABELS_FOUND": "No labels available yet.",
"SELECTED_COUNT": "{count} selected",
"CLEAR_SELECTION": "Clear selection",
"SELECT_ALL": "Select all ({count})"
"SELECT_ALL": "Select all ({count})",
"DELETE_CONTACTS": "Vymazat",
"DELETE_SUCCESS": "Contacts deleted successfully.",
"DELETE_FAILED": "Failed to delete contacts.",
"DELETE_DIALOG": {
"TITLE": "Delete selected contacts",
"SINGULAR_TITLE": "Delete selected contact",
"DESCRIPTION": "This will permanently delete {count} selected contacts. This action cannot be undone.",
"SINGULAR_DESCRIPTION": "This will permanently delete the selected contact. This action cannot be undone.",
"CONFIRM_MULTIPLE": "Delete contacts",
"CONFIRM_SINGLE": "Delete contact"
}
},
"COMPOSE_NEW_CONVERSATION": {
"CONTACT_SEARCH": {
@@ -579,7 +579,18 @@
"NO_LABELS_FOUND": "No labels available yet.",
"SELECTED_COUNT": "{count} selected",
"CLEAR_SELECTION": "Clear selection",
"SELECT_ALL": "Select all ({count})"
"SELECT_ALL": "Select all ({count})",
"DELETE_CONTACTS": "Slet",
"DELETE_SUCCESS": "Contacts deleted successfully.",
"DELETE_FAILED": "Failed to delete contacts.",
"DELETE_DIALOG": {
"TITLE": "Delete selected contacts",
"SINGULAR_TITLE": "Delete selected contact",
"DESCRIPTION": "This will permanently delete {count} selected contacts. This action cannot be undone.",
"SINGULAR_DESCRIPTION": "This will permanently delete the selected contact. This action cannot be undone.",
"CONFIRM_MULTIPLE": "Delete contacts",
"CONFIRM_SINGLE": "Slet kontakt"
}
},
"COMPOSE_NEW_CONVERSATION": {
"CONTACT_SEARCH": {
@@ -579,7 +579,18 @@
"NO_LABELS_FOUND": "No labels available yet.",
"SELECTED_COUNT": "{count} selected",
"CLEAR_SELECTION": "Clear selection",
"SELECT_ALL": "Select all ({count})"
"SELECT_ALL": "Select all ({count})",
"DELETE_CONTACTS": "Löschen",
"DELETE_SUCCESS": "Contacts deleted successfully.",
"DELETE_FAILED": "Failed to delete contacts.",
"DELETE_DIALOG": {
"TITLE": "Delete selected contacts",
"SINGULAR_TITLE": "Delete selected contact",
"DESCRIPTION": "This will permanently delete {count} selected contacts. This action cannot be undone.",
"SINGULAR_DESCRIPTION": "This will permanently delete the selected contact. This action cannot be undone.",
"CONFIRM_MULTIPLE": "Delete contacts",
"CONFIRM_SINGLE": "Kontakt löschen"
}
},
"COMPOSE_NEW_CONVERSATION": {
"CONTACT_SEARCH": {
@@ -579,7 +579,18 @@
"NO_LABELS_FOUND": "No labels available yet.",
"SELECTED_COUNT": "{count} selected",
"CLEAR_SELECTION": "Clear selection",
"SELECT_ALL": "Select all ({count})"
"SELECT_ALL": "Select all ({count})",
"DELETE_CONTACTS": "Διαγραφή",
"DELETE_SUCCESS": "Contacts deleted successfully.",
"DELETE_FAILED": "Failed to delete contacts.",
"DELETE_DIALOG": {
"TITLE": "Delete selected contacts",
"SINGULAR_TITLE": "Delete selected contact",
"DESCRIPTION": "This will permanently delete {count} selected contacts. This action cannot be undone.",
"SINGULAR_DESCRIPTION": "This will permanently delete the selected contact. This action cannot be undone.",
"CONFIRM_MULTIPLE": "Delete contacts",
"CONFIRM_SINGLE": "Διαγραφή Επαφής"
}
},
"COMPOSE_NEW_CONVERSATION": {
"CONTACT_SEARCH": {
@@ -46,6 +46,10 @@
"CONVERSATION_TYPING_OFF": "Conversation Typing Off"
}
},
"NAME": {
"LABEL": "Webhook Name",
"PLACEHOLDER": "Enter the name of the webhook"
},
"END_POINT": {
"LABEL": "Webhook URL",
"PLACEHOLDER": "Example: {webhookExampleURL}",
@@ -53,6 +53,8 @@
"LAST_7_DAYS": "Last 7 days",
"LAST_14_DAYS": "Last 14 days",
"LAST_30_DAYS": "Last 30 days",
"THIS_MONTH": "This month",
"LAST_MONTH": "Last month",
"LAST_3_MONTHS": "Last 3 months",
"LAST_6_MONTHS": "Last 6 months",
"LAST_YEAR": "Last year",
@@ -579,7 +579,18 @@
"NO_LABELS_FOUND": "No labels available yet.",
"SELECTED_COUNT": "{count} selected",
"CLEAR_SELECTION": "Clear selection",
"SELECT_ALL": "Select all ({count})"
"SELECT_ALL": "Select all ({count})",
"DELETE_CONTACTS": "Eliminar",
"DELETE_SUCCESS": "Contacts deleted successfully.",
"DELETE_FAILED": "Failed to delete contacts.",
"DELETE_DIALOG": {
"TITLE": "Delete selected contacts",
"SINGULAR_TITLE": "Delete selected contact",
"DESCRIPTION": "This will permanently delete {count} selected contacts. This action cannot be undone.",
"SINGULAR_DESCRIPTION": "This will permanently delete the selected contact. This action cannot be undone.",
"CONFIRM_MULTIPLE": "Delete contacts",
"CONFIRM_SINGLE": "Eliminar contacto"
}
},
"COMPOSE_NEW_CONVERSATION": {
"CONTACT_SEARCH": {
@@ -579,7 +579,18 @@
"NO_LABELS_FOUND": "No labels available yet.",
"SELECTED_COUNT": "{count} selected",
"CLEAR_SELECTION": "Clear selection",
"SELECT_ALL": "Select all ({count})"
"SELECT_ALL": "Select all ({count})",
"DELETE_CONTACTS": "Delete",
"DELETE_SUCCESS": "Contacts deleted successfully.",
"DELETE_FAILED": "Failed to delete contacts.",
"DELETE_DIALOG": {
"TITLE": "Delete selected contacts",
"SINGULAR_TITLE": "Delete selected contact",
"DESCRIPTION": "This will permanently delete {count} selected contacts. This action cannot be undone.",
"SINGULAR_DESCRIPTION": "This will permanently delete the selected contact. This action cannot be undone.",
"CONFIRM_MULTIPLE": "Delete contacts",
"CONFIRM_SINGLE": "Delete contact"
}
},
"COMPOSE_NEW_CONVERSATION": {
"CONTACT_SEARCH": {
@@ -579,7 +579,18 @@
"NO_LABELS_FOUND": "No labels available yet.",
"SELECTED_COUNT": "{count} selected",
"CLEAR_SELECTION": "Clear selection",
"SELECT_ALL": "Select all ({count})"
"SELECT_ALL": "Select all ({count})",
"DELETE_CONTACTS": "حذف",
"DELETE_SUCCESS": "Contacts deleted successfully.",
"DELETE_FAILED": "Failed to delete contacts.",
"DELETE_DIALOG": {
"TITLE": "Delete selected contacts",
"SINGULAR_TITLE": "Delete selected contact",
"DESCRIPTION": "This will permanently delete {count} selected contacts. This action cannot be undone.",
"SINGULAR_DESCRIPTION": "This will permanently delete the selected contact. This action cannot be undone.",
"CONFIRM_MULTIPLE": "Delete contacts",
"CONFIRM_SINGLE": "حذف مخاطب"
}
},
"COMPOSE_NEW_CONVERSATION": {
"CONTACT_SEARCH": {
@@ -579,7 +579,18 @@
"NO_LABELS_FOUND": "No labels available yet.",
"SELECTED_COUNT": "{count} selected",
"CLEAR_SELECTION": "Clear selection",
"SELECT_ALL": "Select all ({count})"
"SELECT_ALL": "Select all ({count})",
"DELETE_CONTACTS": "Poista",
"DELETE_SUCCESS": "Contacts deleted successfully.",
"DELETE_FAILED": "Failed to delete contacts.",
"DELETE_DIALOG": {
"TITLE": "Delete selected contacts",
"SINGULAR_TITLE": "Delete selected contact",
"DESCRIPTION": "This will permanently delete {count} selected contacts. This action cannot be undone.",
"SINGULAR_DESCRIPTION": "This will permanently delete the selected contact. This action cannot be undone.",
"CONFIRM_MULTIPLE": "Delete contacts",
"CONFIRM_SINGLE": "Poista yhteystieto"
}
},
"COMPOSE_NEW_CONVERSATION": {
"CONTACT_SEARCH": {
@@ -579,7 +579,18 @@
"NO_LABELS_FOUND": "No labels available yet.",
"SELECTED_COUNT": "{count} selected",
"CLEAR_SELECTION": "Clear selection",
"SELECT_ALL": "Select all ({count})"
"SELECT_ALL": "Select all ({count})",
"DELETE_CONTACTS": "Supprimer",
"DELETE_SUCCESS": "Contacts deleted successfully.",
"DELETE_FAILED": "Failed to delete contacts.",
"DELETE_DIALOG": {
"TITLE": "Delete selected contacts",
"SINGULAR_TITLE": "Delete selected contact",
"DESCRIPTION": "This will permanently delete {count} selected contacts. This action cannot be undone.",
"SINGULAR_DESCRIPTION": "This will permanently delete the selected contact. This action cannot be undone.",
"CONFIRM_MULTIPLE": "Delete contacts",
"CONFIRM_SINGLE": "Supprimer le contact"
}
},
"COMPOSE_NEW_CONVERSATION": {
"CONTACT_SEARCH": {
@@ -579,7 +579,18 @@
"NO_LABELS_FOUND": "No labels available yet.",
"SELECTED_COUNT": "{count} selected",
"CLEAR_SELECTION": "Clear selection",
"SELECT_ALL": "Select all ({count})"
"SELECT_ALL": "Select all ({count})",
"DELETE_CONTACTS": "מחק",
"DELETE_SUCCESS": "Contacts deleted successfully.",
"DELETE_FAILED": "Failed to delete contacts.",
"DELETE_DIALOG": {
"TITLE": "Delete selected contacts",
"SINGULAR_TITLE": "Delete selected contact",
"DESCRIPTION": "This will permanently delete {count} selected contacts. This action cannot be undone.",
"SINGULAR_DESCRIPTION": "This will permanently delete the selected contact. This action cannot be undone.",
"CONFIRM_MULTIPLE": "Delete contacts",
"CONFIRM_SINGLE": "מחק איש קשר"
}
},
"COMPOSE_NEW_CONVERSATION": {
"CONTACT_SEARCH": {
@@ -579,7 +579,18 @@
"NO_LABELS_FOUND": "No labels available yet.",
"SELECTED_COUNT": "{count} selected",
"CLEAR_SELECTION": "Clear selection",
"SELECT_ALL": "Select all ({count})"
"SELECT_ALL": "Select all ({count})",
"DELETE_CONTACTS": "Delete",
"DELETE_SUCCESS": "Contacts deleted successfully.",
"DELETE_FAILED": "Failed to delete contacts.",
"DELETE_DIALOG": {
"TITLE": "Delete selected contacts",
"SINGULAR_TITLE": "Delete selected contact",
"DESCRIPTION": "This will permanently delete {count} selected contacts. This action cannot be undone.",
"SINGULAR_DESCRIPTION": "This will permanently delete the selected contact. This action cannot be undone.",
"CONFIRM_MULTIPLE": "Delete contacts",
"CONFIRM_SINGLE": "Delete contact"
}
},
"COMPOSE_NEW_CONVERSATION": {
"CONTACT_SEARCH": {
@@ -579,7 +579,18 @@
"NO_LABELS_FOUND": "No labels available yet.",
"SELECTED_COUNT": "{count} selected",
"CLEAR_SELECTION": "Clear selection",
"SELECT_ALL": "Select all ({count})"
"SELECT_ALL": "Select all ({count})",
"DELETE_CONTACTS": "Izbriši",
"DELETE_SUCCESS": "Contacts deleted successfully.",
"DELETE_FAILED": "Failed to delete contacts.",
"DELETE_DIALOG": {
"TITLE": "Delete selected contacts",
"SINGULAR_TITLE": "Delete selected contact",
"DESCRIPTION": "This will permanently delete {count} selected contacts. This action cannot be undone.",
"SINGULAR_DESCRIPTION": "This will permanently delete the selected contact. This action cannot be undone.",
"CONFIRM_MULTIPLE": "Delete contacts",
"CONFIRM_SINGLE": "Delete contact"
}
},
"COMPOSE_NEW_CONVERSATION": {
"CONTACT_SEARCH": {
@@ -579,7 +579,18 @@
"NO_LABELS_FOUND": "No labels available yet.",
"SELECTED_COUNT": "{count} selected",
"CLEAR_SELECTION": "Clear selection",
"SELECT_ALL": "Select all ({count})"
"SELECT_ALL": "Select all ({count})",
"DELETE_CONTACTS": "Törlés",
"DELETE_SUCCESS": "Contacts deleted successfully.",
"DELETE_FAILED": "Failed to delete contacts.",
"DELETE_DIALOG": {
"TITLE": "Delete selected contacts",
"SINGULAR_TITLE": "Delete selected contact",
"DESCRIPTION": "This will permanently delete {count} selected contacts. This action cannot be undone.",
"SINGULAR_DESCRIPTION": "This will permanently delete the selected contact. This action cannot be undone.",
"CONFIRM_MULTIPLE": "Delete contacts",
"CONFIRM_SINGLE": "Kontakt törlése"
}
},
"COMPOSE_NEW_CONVERSATION": {
"CONTACT_SEARCH": {
@@ -579,7 +579,18 @@
"NO_LABELS_FOUND": "No labels available yet.",
"SELECTED_COUNT": "{count} selected",
"CLEAR_SELECTION": "Clear selection",
"SELECT_ALL": "Select all ({count})"
"SELECT_ALL": "Select all ({count})",
"DELETE_CONTACTS": "Delete",
"DELETE_SUCCESS": "Contacts deleted successfully.",
"DELETE_FAILED": "Failed to delete contacts.",
"DELETE_DIALOG": {
"TITLE": "Delete selected contacts",
"SINGULAR_TITLE": "Delete selected contact",
"DESCRIPTION": "This will permanently delete {count} selected contacts. This action cannot be undone.",
"SINGULAR_DESCRIPTION": "This will permanently delete the selected contact. This action cannot be undone.",
"CONFIRM_MULTIPLE": "Delete contacts",
"CONFIRM_SINGLE": "Delete contact"
}
},
"COMPOSE_NEW_CONVERSATION": {
"CONTACT_SEARCH": {
@@ -579,7 +579,18 @@
"NO_LABELS_FOUND": "No labels available yet.",
"SELECTED_COUNT": "{count} selected",
"CLEAR_SELECTION": "Clear selection",
"SELECT_ALL": "Select all ({count})"
"SELECT_ALL": "Select all ({count})",
"DELETE_CONTACTS": "Hapus",
"DELETE_SUCCESS": "Contacts deleted successfully.",
"DELETE_FAILED": "Failed to delete contacts.",
"DELETE_DIALOG": {
"TITLE": "Delete selected contacts",
"SINGULAR_TITLE": "Delete selected contact",
"DESCRIPTION": "This will permanently delete {count} selected contacts. This action cannot be undone.",
"SINGULAR_DESCRIPTION": "This will permanently delete the selected contact. This action cannot be undone.",
"CONFIRM_MULTIPLE": "Delete contacts",
"CONFIRM_SINGLE": "Hapus kontak"
}
},
"COMPOSE_NEW_CONVERSATION": {
"CONTACT_SEARCH": {
@@ -579,7 +579,18 @@
"NO_LABELS_FOUND": "No labels available yet.",
"SELECTED_COUNT": "{count} selected",
"CLEAR_SELECTION": "Clear selection",
"SELECT_ALL": "Select all ({count})"
"SELECT_ALL": "Select all ({count})",
"DELETE_CONTACTS": "Eyða",
"DELETE_SUCCESS": "Contacts deleted successfully.",
"DELETE_FAILED": "Failed to delete contacts.",
"DELETE_DIALOG": {
"TITLE": "Delete selected contacts",
"SINGULAR_TITLE": "Delete selected contact",
"DESCRIPTION": "This will permanently delete {count} selected contacts. This action cannot be undone.",
"SINGULAR_DESCRIPTION": "This will permanently delete the selected contact. This action cannot be undone.",
"CONFIRM_MULTIPLE": "Delete contacts",
"CONFIRM_SINGLE": "Eyða tengilið"
}
},
"COMPOSE_NEW_CONVERSATION": {
"CONTACT_SEARCH": {
@@ -13,7 +13,7 @@
"BROWSER": "Browser",
"OS": "Sistema Operativo",
"INITIATED_FROM": "Iniziato da",
"INITIATED_AT": "Iniziato alle",
"INITIATED_AT": "Avviata alle",
"IP_ADDRESS": "Indirizzo IP",
"CREATED_AT_LABEL": "Creato il",
"NEW_MESSAGE": "Nuovo messaggio",
@@ -579,7 +579,18 @@
"NO_LABELS_FOUND": "Nessuna etichetta disponibile.",
"SELECTED_COUNT": "{count} selezionate",
"CLEAR_SELECTION": "Annulla selezione",
"SELECT_ALL": "Seleziona tutto ({count})"
"SELECT_ALL": "Seleziona tutto ({count})",
"DELETE_CONTACTS": "Elimina",
"DELETE_SUCCESS": "Contatti eliminati con successo.",
"DELETE_FAILED": "Impossibile eliminare i contatti.",
"DELETE_DIALOG": {
"TITLE": "Elimina i contatti selezionati",
"SINGULAR_TITLE": "Elimina il contatto selezionato",
"DESCRIPTION": "Questo eliminerà definitivamente i {count} contatti selezionati. Questa azione non può essere annullata.",
"SINGULAR_DESCRIPTION": "Questo eliminerà definitivamente il contatto selezionato. Questa azione non può essere annullata.",
"CONFIRM_MULTIPLE": "Elimina contatti",
"CONFIRM_SINGLE": "Elimina contatto"
}
},
"COMPOSE_NEW_CONVERSATION": {
"CONTACT_SEARCH": {
@@ -1,6 +1,6 @@
{
"CONVERSATION": {
"SELECT_A_CONVERSATION": "Si prega di selezionare una conversazione dal pannello a sinistra",
"SELECT_A_CONVERSATION": "Seleziona una conversazione dal pannello a sinistra",
"CSAT_REPLY_MESSAGE": "Valuta la conversazione",
"404": "Siamo spiacenti, non siamo riusciti a trovare la conversazione. Riprova",
"SWITCH_VIEW_LAYOUT": "Cambia layout",
@@ -6,7 +6,7 @@
"LOADING": "Caricamento notifiche",
"404": "Non ci sono notifiche attive in questo gruppo.",
"NO_NOTIFICATIONS": "Nessuna notifica",
"NOTE": "Notifiche da tutte le inbox sottoscritte",
"NOTE": "Notifiche da tutte le Inbox",
"NO_MESSAGES_AVAILABLE": "Oops! Impossibile recuperare i messaggi",
"SNOOZED_UNTIL": "Posticipata fino a",
"SNOOZED_UNTIL_TOMORROW": "Posticipata fino a domani",
@@ -45,8 +45,8 @@
"MARK_AS_UNREAD": "Segna come da leggere",
"SNOOZE": "Posticipa",
"DELETE": "Elimina",
"MARK_ALL_READ": "Segna tutto come letto",
"DELETE_ALL": "Elimina tutto",
"MARK_ALL_READ": "Segna tutte come lette",
"DELETE_ALL": "Elimina tutte",
"DELETE_ALL_READ": "Elimina tutte le lette"
},
"DISPLAY_MENU": {
@@ -252,7 +252,7 @@
},
"BUSINESS_ACCOUNT_ID": {
"LABEL": "ID Account Business",
"PLACEHOLDER": "Si prega di inserire l'ID dell'account business ottenuto dalla dashboard sviluppatore di Facebook.",
"PLACEHOLDER": "Inserisci l'ID dell'Account Business ottenuto dalla dashboard sviluppatore di Facebook.",
"ERROR": "Inserisci un valore valido."
},
"WEBHOOK_VERIFY_TOKEN": {
@@ -107,7 +107,7 @@
},
"HELP_TEXT": {
"TITLE": "Stai utilizzando l'integrazione Slack",
"BODY": "Con questa integrazione, tutte le conversazioni in arrivo verranno sincronizzate nel canale ***{selectedChannelName}*** del tuo workspace Slack. Potrai gestire tutte le conversazioni con i clienti direttamente dal canale e non perderai mai un messaggio.\n\nEcco le principali funzionalità dellintegrazione:\n\n**Rispondi alle conversazioni direttamente da Slack:** Per rispondere a una conversazione nel canale Slack ***{selectedChannelName}***, ti basta scrivere il tuo messaggio e inviarlo come thread. Questo genererà automaticamente una risposta al cliente tramite Chatwoot. Semplice, no?\n\n**Crea note private:** Se vuoi aggiungere note private invece di risposte, inizia il messaggio con ***`note:`***. In questo modo il messaggio resterà privato e non sarà visibile al cliente.\n\n**Associa un profilo operatore:** Se la persona che risponde su Slack ha un profilo operatore in Chatwoot con lo stesso indirizzo email, le risposte verranno associate automaticamente a quel profilo. In questo modo potrai sapere facilmente chi ha risposto e quando. Se invece chi risponde non ha un profilo operatore associato, le risposte verranno inviate al cliente dal profilo del bot.",
"BODY": "Con questa integrazione, tutte le conversazioni in arrivo verranno sincronizzate nel canale ***{selectedChannelName}*** del tuo workspace Slack. Potrai gestire tutte le conversazioni con i clienti direttamente dal canale e non perderai mai un messaggio.\n\nEcco le principali funzionalità dellintegrazione:\n\n**Rispondi alle conversazioni direttamente da Slack:** Per rispondere a una conversazione nel canale Slack ***{selectedChannelName}***, ti basta scrivere il tuo messaggio e inviarlo come thread. Questo genererà automaticamente una risposta al cliente tramite Chatwoot. Semplice, no?\n\n**Crea note private:** Se vuoi aggiungere note private invece di risposte, inizia il messaggio con ***`note:`***. In questo modo il messaggio resterà privato e non sarà visibile al cliente.\n\n**Associa un profilo operatore:** Se la persona che risponde su Slack ha un profilo operatore in app con lo stesso indirizzo email, le risposte verranno associate automaticamente a quel profilo. In questo modo potrai sapere facilmente chi ha risposto e quando. Se invece chi risponde non ha un profilo operatore associato, le risposte verranno inviate al cliente dal profilo del bot.",
"SELECTED": "selezionato"
},
"SELECT_CHANNEL": {
@@ -472,7 +472,7 @@
"FEATURES": {
"TITLE": "Funzionalità",
"ALLOW_CONVERSATION_FAQS": "Genera FAQ dalle conversazioni risolte",
"ALLOW_MEMORIES": "Cattura i dettagli chiave come memorie dalle interazioni con i clienti.",
"ALLOW_MEMORIES": "Salva memorie e dettagli chiave dalle interazioni con i clienti.",
"ALLOW_CITATIONS": "Includi citazioni alle fonti nelle risposte"
}
},
@@ -694,8 +694,8 @@
"HEADER": "Documents",
"ADD_NEW": "Create a new document",
"RELATED_RESPONSES": {
"TITLE": "Related FAQs",
"DESCRIPTION": "These FAQs are generated directly from the document."
"TITLE": "FAQ Correlate",
"DESCRIPTION": "Queste FAQ vengono generate direttamente dai Documenti."
},
"FORM_DESCRIPTION": "Enter the URL of the document to add it as a knowledge source and choose the assistant to associate it with.",
"CREATE": {
@@ -740,7 +740,7 @@
"ERROR_MESSAGE": "Si è verificato un errore durante l'eliminazione del documento, riprova."
},
"OPTIONS": {
"VIEW_RELATED_RESPONSES": "Visualizza Risposte Correlate",
"VIEW_RELATED_RESPONSES": "Visualizza FAQ Correlate",
"DELETE_DOCUMENT": "Elimina Documento"
},
"EMPTY_STATE": {
@@ -895,7 +895,7 @@
"FILTER": {
"ASSISTANT": "Assistente: {selected}",
"STATUS": "Stato: {selected}",
"ALL_ASSISTANTS": "Tutte"
"ALL_ASSISTANTS": "Tutti"
},
"STATUS": {
"TITLE": "Stato",
@@ -193,7 +193,7 @@
}
},
"LABEL_REPORTS": {
"HEADER": "Panoramica etichette",
"HEADER": "Panoramica Etichette",
"DESCRIPTION": "Monitora la performance delle etichette con metriche chiave, tra cui conversazioni, tempi di risposta, tempi di risoluzione e casi risolti. Fai clic sul nome di un'etichetta per approfondimenti dettagliati.",
"LOADING_CHART": "Caricamento dati grafici...",
"NO_ENOUGH_DATA": "Non ci sono abbastanza dati per generare il report, riprova più tardi.",
@@ -331,7 +331,7 @@
}
},
"TEAM_REPORTS": {
"HEADER": "Panoramica team",
"HEADER": "Panoramica Team",
"DESCRIPTION": "Ottieni un'istantanea delle prestazioni del tuo team con metriche essenziali, tra cui conversazioni, tempi di risposta, tempi di risoluzione e casi risolti. Fare clic su un nome del team per maggiori dettagli.",
"LOADING_CHART": "Caricamento dati grafici...",
"NO_ENOUGH_DATA": "Non ci sono abbastanza dati per generare il report, riprova più tardi.",
@@ -399,7 +399,7 @@
}
},
"CSAT_REPORTS": {
"HEADER": "Rapporti CSAT",
"HEADER": "Report CSAT",
"NO_RECORDS": "Non ci sono risposte ai sondaggi CSAT disponibili.",
"DOWNLOAD": "Scarica report CSAT",
"DOWNLOAD_FAILED": "Download dei report CSAT non riuscito",
@@ -579,7 +579,18 @@
"NO_LABELS_FOUND": "No labels available yet.",
"SELECTED_COUNT": "{count} selected",
"CLEAR_SELECTION": "Clear selection",
"SELECT_ALL": "Select all ({count})"
"SELECT_ALL": "Select all ({count})",
"DELETE_CONTACTS": "削除",
"DELETE_SUCCESS": "Contacts deleted successfully.",
"DELETE_FAILED": "Failed to delete contacts.",
"DELETE_DIALOG": {
"TITLE": "Delete selected contacts",
"SINGULAR_TITLE": "Delete selected contact",
"DESCRIPTION": "This will permanently delete {count} selected contacts. This action cannot be undone.",
"SINGULAR_DESCRIPTION": "This will permanently delete the selected contact. This action cannot be undone.",
"CONFIRM_MULTIPLE": "Delete contacts",
"CONFIRM_SINGLE": "連絡先を削除"
}
},
"COMPOSE_NEW_CONVERSATION": {
"CONTACT_SEARCH": {
@@ -579,7 +579,18 @@
"NO_LABELS_FOUND": "No labels available yet.",
"SELECTED_COUNT": "{count} selected",
"CLEAR_SELECTION": "Clear selection",
"SELECT_ALL": "Select all ({count})"
"SELECT_ALL": "Select all ({count})",
"DELETE_CONTACTS": "Delete",
"DELETE_SUCCESS": "Contacts deleted successfully.",
"DELETE_FAILED": "Failed to delete contacts.",
"DELETE_DIALOG": {
"TITLE": "Delete selected contacts",
"SINGULAR_TITLE": "Delete selected contact",
"DESCRIPTION": "This will permanently delete {count} selected contacts. This action cannot be undone.",
"SINGULAR_DESCRIPTION": "This will permanently delete the selected contact. This action cannot be undone.",
"CONFIRM_MULTIPLE": "Delete contacts",
"CONFIRM_SINGLE": "Delete contact"
}
},
"COMPOSE_NEW_CONVERSATION": {
"CONTACT_SEARCH": {
@@ -579,7 +579,18 @@
"NO_LABELS_FOUND": "No labels available yet.",
"SELECTED_COUNT": "{count} selected",
"CLEAR_SELECTION": "Clear selection",
"SELECT_ALL": "Select all ({count})"
"SELECT_ALL": "Select all ({count})",
"DELETE_CONTACTS": "삭제",
"DELETE_SUCCESS": "Contacts deleted successfully.",
"DELETE_FAILED": "Failed to delete contacts.",
"DELETE_DIALOG": {
"TITLE": "Delete selected contacts",
"SINGULAR_TITLE": "Delete selected contact",
"DESCRIPTION": "This will permanently delete {count} selected contacts. This action cannot be undone.",
"SINGULAR_DESCRIPTION": "This will permanently delete the selected contact. This action cannot be undone.",
"CONFIRM_MULTIPLE": "Delete contacts",
"CONFIRM_SINGLE": "연락처 지우기"
}
},
"COMPOSE_NEW_CONVERSATION": {
"CONTACT_SEARCH": {
@@ -579,7 +579,18 @@
"NO_LABELS_FOUND": "No labels available yet.",
"SELECTED_COUNT": "{count} selected",
"CLEAR_SELECTION": "Clear selection",
"SELECT_ALL": "Select all ({count})"
"SELECT_ALL": "Select all ({count})",
"DELETE_CONTACTS": "Ištrinti",
"DELETE_SUCCESS": "Contacts deleted successfully.",
"DELETE_FAILED": "Failed to delete contacts.",
"DELETE_DIALOG": {
"TITLE": "Delete selected contacts",
"SINGULAR_TITLE": "Delete selected contact",
"DESCRIPTION": "This will permanently delete {count} selected contacts. This action cannot be undone.",
"SINGULAR_DESCRIPTION": "This will permanently delete the selected contact. This action cannot be undone.",
"CONFIRM_MULTIPLE": "Delete contacts",
"CONFIRM_SINGLE": "Trinti kontaktą"
}
},
"COMPOSE_NEW_CONVERSATION": {
"CONTACT_SEARCH": {
@@ -579,7 +579,18 @@
"NO_LABELS_FOUND": "No labels available yet.",
"SELECTED_COUNT": "Atlasīti {count}",
"CLEAR_SELECTION": "Clear selection",
"SELECT_ALL": "Select all ({count})"
"SELECT_ALL": "Select all ({count})",
"DELETE_CONTACTS": "Dzēst",
"DELETE_SUCCESS": "Contacts deleted successfully.",
"DELETE_FAILED": "Failed to delete contacts.",
"DELETE_DIALOG": {
"TITLE": "Delete selected contacts",
"SINGULAR_TITLE": "Delete selected contact",
"DESCRIPTION": "This will permanently delete {count} selected contacts. This action cannot be undone.",
"SINGULAR_DESCRIPTION": "This will permanently delete the selected contact. This action cannot be undone.",
"CONFIRM_MULTIPLE": "Delete contacts",
"CONFIRM_SINGLE": "Dzēst kontaktpersonu"
}
},
"COMPOSE_NEW_CONVERSATION": {
"CONTACT_SEARCH": {
@@ -579,7 +579,18 @@
"NO_LABELS_FOUND": "No labels available yet.",
"SELECTED_COUNT": "{count} selected",
"CLEAR_SELECTION": "Clear selection",
"SELECT_ALL": "Select all ({count})"
"SELECT_ALL": "Select all ({count})",
"DELETE_CONTACTS": "ഇല്ലാതാക്കുക",
"DELETE_SUCCESS": "Contacts deleted successfully.",
"DELETE_FAILED": "Failed to delete contacts.",
"DELETE_DIALOG": {
"TITLE": "Delete selected contacts",
"SINGULAR_TITLE": "Delete selected contact",
"DESCRIPTION": "This will permanently delete {count} selected contacts. This action cannot be undone.",
"SINGULAR_DESCRIPTION": "This will permanently delete the selected contact. This action cannot be undone.",
"CONFIRM_MULTIPLE": "Delete contacts",
"CONFIRM_SINGLE": "കോൺടാക്റ്റ് ഇല്ലാതാക്കുക"
}
},
"COMPOSE_NEW_CONVERSATION": {
"CONTACT_SEARCH": {
@@ -579,7 +579,18 @@
"NO_LABELS_FOUND": "No labels available yet.",
"SELECTED_COUNT": "{count} selected",
"CLEAR_SELECTION": "Clear selection",
"SELECT_ALL": "Select all ({count})"
"SELECT_ALL": "Select all ({count})",
"DELETE_CONTACTS": "Padamkan",
"DELETE_SUCCESS": "Contacts deleted successfully.",
"DELETE_FAILED": "Failed to delete contacts.",
"DELETE_DIALOG": {
"TITLE": "Delete selected contacts",
"SINGULAR_TITLE": "Delete selected contact",
"DESCRIPTION": "This will permanently delete {count} selected contacts. This action cannot be undone.",
"SINGULAR_DESCRIPTION": "This will permanently delete the selected contact. This action cannot be undone.",
"CONFIRM_MULTIPLE": "Delete contacts",
"CONFIRM_SINGLE": "Delete contact"
}
},
"COMPOSE_NEW_CONVERSATION": {
"CONTACT_SEARCH": {
@@ -579,7 +579,18 @@
"NO_LABELS_FOUND": "No labels available yet.",
"SELECTED_COUNT": "{count} selected",
"CLEAR_SELECTION": "Clear selection",
"SELECT_ALL": "Select all ({count})"
"SELECT_ALL": "Select all ({count})",
"DELETE_CONTACTS": "Delete",
"DELETE_SUCCESS": "Contacts deleted successfully.",
"DELETE_FAILED": "Failed to delete contacts.",
"DELETE_DIALOG": {
"TITLE": "Delete selected contacts",
"SINGULAR_TITLE": "Delete selected contact",
"DESCRIPTION": "This will permanently delete {count} selected contacts. This action cannot be undone.",
"SINGULAR_DESCRIPTION": "This will permanently delete the selected contact. This action cannot be undone.",
"CONFIRM_MULTIPLE": "Delete contacts",
"CONFIRM_SINGLE": "Delete contact"
}
},
"COMPOSE_NEW_CONVERSATION": {
"CONTACT_SEARCH": {
@@ -579,7 +579,18 @@
"NO_LABELS_FOUND": "No labels available yet.",
"SELECTED_COUNT": "{count} selected",
"CLEAR_SELECTION": "Clear selection",
"SELECT_ALL": "Select all ({count})"
"SELECT_ALL": "Select all ({count})",
"DELETE_CONTACTS": "Verwijderen",
"DELETE_SUCCESS": "Contacts deleted successfully.",
"DELETE_FAILED": "Failed to delete contacts.",
"DELETE_DIALOG": {
"TITLE": "Delete selected contacts",
"SINGULAR_TITLE": "Delete selected contact",
"DESCRIPTION": "This will permanently delete {count} selected contacts. This action cannot be undone.",
"SINGULAR_DESCRIPTION": "This will permanently delete the selected contact. This action cannot be undone.",
"CONFIRM_MULTIPLE": "Delete contacts",
"CONFIRM_SINGLE": "Contactpersoon verwijderen"
}
},
"COMPOSE_NEW_CONVERSATION": {
"CONTACT_SEARCH": {
@@ -579,7 +579,18 @@
"NO_LABELS_FOUND": "No labels available yet.",
"SELECTED_COUNT": "{count} selected",
"CLEAR_SELECTION": "Clear selection",
"SELECT_ALL": "Select all ({count})"
"SELECT_ALL": "Select all ({count})",
"DELETE_CONTACTS": "Slett",
"DELETE_SUCCESS": "Contacts deleted successfully.",
"DELETE_FAILED": "Failed to delete contacts.",
"DELETE_DIALOG": {
"TITLE": "Delete selected contacts",
"SINGULAR_TITLE": "Delete selected contact",
"DESCRIPTION": "This will permanently delete {count} selected contacts. This action cannot be undone.",
"SINGULAR_DESCRIPTION": "This will permanently delete the selected contact. This action cannot be undone.",
"CONFIRM_MULTIPLE": "Delete contacts",
"CONFIRM_SINGLE": "Delete contact"
}
},
"COMPOSE_NEW_CONVERSATION": {
"CONTACT_SEARCH": {
@@ -579,7 +579,18 @@
"NO_LABELS_FOUND": "No labels available yet.",
"SELECTED_COUNT": "{count} selected",
"CLEAR_SELECTION": "Clear selection",
"SELECT_ALL": "Select all ({count})"
"SELECT_ALL": "Select all ({count})",
"DELETE_CONTACTS": "Usuń",
"DELETE_SUCCESS": "Contacts deleted successfully.",
"DELETE_FAILED": "Failed to delete contacts.",
"DELETE_DIALOG": {
"TITLE": "Delete selected contacts",
"SINGULAR_TITLE": "Delete selected contact",
"DESCRIPTION": "This will permanently delete {count} selected contacts. This action cannot be undone.",
"SINGULAR_DESCRIPTION": "This will permanently delete the selected contact. This action cannot be undone.",
"CONFIRM_MULTIPLE": "Delete contacts",
"CONFIRM_SINGLE": "Usuń kontakt"
}
},
"COMPOSE_NEW_CONVERSATION": {
"CONTACT_SEARCH": {
@@ -579,7 +579,18 @@
"NO_LABELS_FOUND": "No labels available yet.",
"SELECTED_COUNT": "{count} selected",
"CLEAR_SELECTION": "Clear selection",
"SELECT_ALL": "Selecionar todas ({count})"
"SELECT_ALL": "Selecionar todas ({count})",
"DELETE_CONTACTS": "Excluir",
"DELETE_SUCCESS": "Contacts deleted successfully.",
"DELETE_FAILED": "Failed to delete contacts.",
"DELETE_DIALOG": {
"TITLE": "Delete selected contacts",
"SINGULAR_TITLE": "Delete selected contact",
"DESCRIPTION": "This will permanently delete {count} selected contacts. This action cannot be undone.",
"SINGULAR_DESCRIPTION": "This will permanently delete the selected contact. This action cannot be undone.",
"CONFIRM_MULTIPLE": "Delete contacts",
"CONFIRM_SINGLE": "Excluir contacto"
}
},
"COMPOSE_NEW_CONVERSATION": {
"CONTACT_SEARCH": {
@@ -579,7 +579,18 @@
"NO_LABELS_FOUND": "No labels available yet.",
"SELECTED_COUNT": "{count} selecionado",
"CLEAR_SELECTION": "Clear selection",
"SELECT_ALL": "Selecionar todos ({count})"
"SELECT_ALL": "Selecionar todos ({count})",
"DELETE_CONTACTS": "Excluir",
"DELETE_SUCCESS": "Contacts deleted successfully.",
"DELETE_FAILED": "Failed to delete contacts.",
"DELETE_DIALOG": {
"TITLE": "Delete selected contacts",
"SINGULAR_TITLE": "Delete selected contact",
"DESCRIPTION": "This will permanently delete {count} selected contacts. This action cannot be undone.",
"SINGULAR_DESCRIPTION": "This will permanently delete the selected contact. This action cannot be undone.",
"CONFIRM_MULTIPLE": "Delete contacts",
"CONFIRM_SINGLE": "Excluir contato"
}
},
"COMPOSE_NEW_CONVERSATION": {
"CONTACT_SEARCH": {
@@ -579,7 +579,18 @@
"NO_LABELS_FOUND": "No labels available yet.",
"SELECTED_COUNT": "{count} selected",
"CLEAR_SELECTION": "Clear selection",
"SELECT_ALL": "Select all ({count})"
"SELECT_ALL": "Select all ({count})",
"DELETE_CONTACTS": "Şterge",
"DELETE_SUCCESS": "Contacts deleted successfully.",
"DELETE_FAILED": "Failed to delete contacts.",
"DELETE_DIALOG": {
"TITLE": "Delete selected contacts",
"SINGULAR_TITLE": "Delete selected contact",
"DESCRIPTION": "This will permanently delete {count} selected contacts. This action cannot be undone.",
"SINGULAR_DESCRIPTION": "This will permanently delete the selected contact. This action cannot be undone.",
"CONFIRM_MULTIPLE": "Delete contacts",
"CONFIRM_SINGLE": "Șterge contact"
}
},
"COMPOSE_NEW_CONVERSATION": {
"CONTACT_SEARCH": {
@@ -579,7 +579,18 @@
"NO_LABELS_FOUND": "No labels available yet.",
"SELECTED_COUNT": "Выбрано {count}",
"CLEAR_SELECTION": "Clear selection",
"SELECT_ALL": "Выбрать все ({count})"
"SELECT_ALL": "Выбрать все ({count})",
"DELETE_CONTACTS": "Удалить",
"DELETE_SUCCESS": "Contacts deleted successfully.",
"DELETE_FAILED": "Failed to delete contacts.",
"DELETE_DIALOG": {
"TITLE": "Delete selected contacts",
"SINGULAR_TITLE": "Delete selected contact",
"DESCRIPTION": "This will permanently delete {count} selected contacts. This action cannot be undone.",
"SINGULAR_DESCRIPTION": "This will permanently delete the selected contact. This action cannot be undone.",
"CONFIRM_MULTIPLE": "Delete contacts",
"CONFIRM_SINGLE": "Удалить контакт"
}
},
"COMPOSE_NEW_CONVERSATION": {
"CONTACT_SEARCH": {
@@ -579,7 +579,18 @@
"NO_LABELS_FOUND": "No labels available yet.",
"SELECTED_COUNT": "{count} selected",
"CLEAR_SELECTION": "Clear selection",
"SELECT_ALL": "Select all ({count})"
"SELECT_ALL": "Select all ({count})",
"DELETE_CONTACTS": "Delete",
"DELETE_SUCCESS": "Contacts deleted successfully.",
"DELETE_FAILED": "Failed to delete contacts.",
"DELETE_DIALOG": {
"TITLE": "Delete selected contacts",
"SINGULAR_TITLE": "Delete selected contact",
"DESCRIPTION": "This will permanently delete {count} selected contacts. This action cannot be undone.",
"SINGULAR_DESCRIPTION": "This will permanently delete the selected contact. This action cannot be undone.",
"CONFIRM_MULTIPLE": "Delete contacts",
"CONFIRM_SINGLE": "Delete contact"
}
},
"COMPOSE_NEW_CONVERSATION": {
"CONTACT_SEARCH": {
@@ -579,7 +579,18 @@
"NO_LABELS_FOUND": "No labels available yet.",
"SELECTED_COUNT": "{count} selected",
"CLEAR_SELECTION": "Clear selection",
"SELECT_ALL": "Select all ({count})"
"SELECT_ALL": "Select all ({count})",
"DELETE_CONTACTS": "Vymazať",
"DELETE_SUCCESS": "Contacts deleted successfully.",
"DELETE_FAILED": "Failed to delete contacts.",
"DELETE_DIALOG": {
"TITLE": "Delete selected contacts",
"SINGULAR_TITLE": "Delete selected contact",
"DESCRIPTION": "This will permanently delete {count} selected contacts. This action cannot be undone.",
"SINGULAR_DESCRIPTION": "This will permanently delete the selected contact. This action cannot be undone.",
"CONFIRM_MULTIPLE": "Delete contacts",
"CONFIRM_SINGLE": "Vymazať kontakt"
}
},
"COMPOSE_NEW_CONVERSATION": {
"CONTACT_SEARCH": {
@@ -579,7 +579,18 @@
"NO_LABELS_FOUND": "No labels available yet.",
"SELECTED_COUNT": "{count} selected",
"CLEAR_SELECTION": "Clear selection",
"SELECT_ALL": "Select all ({count})"
"SELECT_ALL": "Select all ({count})",
"DELETE_CONTACTS": "Delete",
"DELETE_SUCCESS": "Contacts deleted successfully.",
"DELETE_FAILED": "Failed to delete contacts.",
"DELETE_DIALOG": {
"TITLE": "Delete selected contacts",
"SINGULAR_TITLE": "Delete selected contact",
"DESCRIPTION": "This will permanently delete {count} selected contacts. This action cannot be undone.",
"SINGULAR_DESCRIPTION": "This will permanently delete the selected contact. This action cannot be undone.",
"CONFIRM_MULTIPLE": "Delete contacts",
"CONFIRM_SINGLE": "Delete contact"
}
},
"COMPOSE_NEW_CONVERSATION": {
"CONTACT_SEARCH": {
@@ -579,7 +579,18 @@
"NO_LABELS_FOUND": "No labels available yet.",
"SELECTED_COUNT": "{count} selected",
"CLEAR_SELECTION": "Clear selection",
"SELECT_ALL": "Select all ({count})"
"SELECT_ALL": "Select all ({count})",
"DELETE_CONTACTS": "Delete",
"DELETE_SUCCESS": "Contacts deleted successfully.",
"DELETE_FAILED": "Failed to delete contacts.",
"DELETE_DIALOG": {
"TITLE": "Delete selected contacts",
"SINGULAR_TITLE": "Delete selected contact",
"DESCRIPTION": "This will permanently delete {count} selected contacts. This action cannot be undone.",
"SINGULAR_DESCRIPTION": "This will permanently delete the selected contact. This action cannot be undone.",
"CONFIRM_MULTIPLE": "Delete contacts",
"CONFIRM_SINGLE": "Delete contact"
}
},
"COMPOSE_NEW_CONVERSATION": {
"CONTACT_SEARCH": {
@@ -579,7 +579,18 @@
"NO_LABELS_FOUND": "No labels available yet.",
"SELECTED_COUNT": "{count} selected",
"CLEAR_SELECTION": "Clear selection",
"SELECT_ALL": "Select all ({count})"
"SELECT_ALL": "Select all ({count})",
"DELETE_CONTACTS": "Izbriši",
"DELETE_SUCCESS": "Contacts deleted successfully.",
"DELETE_FAILED": "Failed to delete contacts.",
"DELETE_DIALOG": {
"TITLE": "Delete selected contacts",
"SINGULAR_TITLE": "Delete selected contact",
"DESCRIPTION": "This will permanently delete {count} selected contacts. This action cannot be undone.",
"SINGULAR_DESCRIPTION": "This will permanently delete the selected contact. This action cannot be undone.",
"CONFIRM_MULTIPLE": "Delete contacts",
"CONFIRM_SINGLE": "Obriši kontakt"
}
},
"COMPOSE_NEW_CONVERSATION": {
"CONTACT_SEARCH": {
@@ -579,7 +579,18 @@
"NO_LABELS_FOUND": "No labels available yet.",
"SELECTED_COUNT": "{count} selected",
"CLEAR_SELECTION": "Clear selection",
"SELECT_ALL": "Select all ({count})"
"SELECT_ALL": "Select all ({count})",
"DELETE_CONTACTS": "Radera",
"DELETE_SUCCESS": "Contacts deleted successfully.",
"DELETE_FAILED": "Failed to delete contacts.",
"DELETE_DIALOG": {
"TITLE": "Delete selected contacts",
"SINGULAR_TITLE": "Delete selected contact",
"DESCRIPTION": "This will permanently delete {count} selected contacts. This action cannot be undone.",
"SINGULAR_DESCRIPTION": "This will permanently delete the selected contact. This action cannot be undone.",
"CONFIRM_MULTIPLE": "Delete contacts",
"CONFIRM_SINGLE": "Ta bort kontakt"
}
},
"COMPOSE_NEW_CONVERSATION": {
"CONTACT_SEARCH": {
@@ -579,7 +579,18 @@
"NO_LABELS_FOUND": "No labels available yet.",
"SELECTED_COUNT": "{count} selected",
"CLEAR_SELECTION": "Clear selection",
"SELECT_ALL": "Select all ({count})"
"SELECT_ALL": "Select all ({count})",
"DELETE_CONTACTS": "Delete",
"DELETE_SUCCESS": "Contacts deleted successfully.",
"DELETE_FAILED": "Failed to delete contacts.",
"DELETE_DIALOG": {
"TITLE": "Delete selected contacts",
"SINGULAR_TITLE": "Delete selected contact",
"DESCRIPTION": "This will permanently delete {count} selected contacts. This action cannot be undone.",
"SINGULAR_DESCRIPTION": "This will permanently delete the selected contact. This action cannot be undone.",
"CONFIRM_MULTIPLE": "Delete contacts",
"CONFIRM_SINGLE": "Delete contact"
}
},
"COMPOSE_NEW_CONVERSATION": {
"CONTACT_SEARCH": {
@@ -579,7 +579,18 @@
"NO_LABELS_FOUND": "No labels available yet.",
"SELECTED_COUNT": "{count} selected",
"CLEAR_SELECTION": "Clear selection",
"SELECT_ALL": "Select all ({count})"
"SELECT_ALL": "Select all ({count})",
"DELETE_CONTACTS": "ลบ",
"DELETE_SUCCESS": "Contacts deleted successfully.",
"DELETE_FAILED": "Failed to delete contacts.",
"DELETE_DIALOG": {
"TITLE": "Delete selected contacts",
"SINGULAR_TITLE": "Delete selected contact",
"DESCRIPTION": "This will permanently delete {count} selected contacts. This action cannot be undone.",
"SINGULAR_DESCRIPTION": "This will permanently delete the selected contact. This action cannot be undone.",
"CONFIRM_MULTIPLE": "Delete contacts",
"CONFIRM_SINGLE": "ลบผู้ติดต่อ"
}
},
"COMPOSE_NEW_CONVERSATION": {
"CONTACT_SEARCH": {
@@ -579,7 +579,18 @@
"NO_LABELS_FOUND": "No labels available yet.",
"SELECTED_COUNT": "{count} selected",
"CLEAR_SELECTION": "Clear selection",
"SELECT_ALL": "Select all ({count})"
"SELECT_ALL": "Select all ({count})",
"DELETE_CONTACTS": "Delete",
"DELETE_SUCCESS": "Contacts deleted successfully.",
"DELETE_FAILED": "Failed to delete contacts.",
"DELETE_DIALOG": {
"TITLE": "Delete selected contacts",
"SINGULAR_TITLE": "Delete selected contact",
"DESCRIPTION": "This will permanently delete {count} selected contacts. This action cannot be undone.",
"SINGULAR_DESCRIPTION": "This will permanently delete the selected contact. This action cannot be undone.",
"CONFIRM_MULTIPLE": "Delete contacts",
"CONFIRM_SINGLE": "Delete contact"
}
},
"COMPOSE_NEW_CONVERSATION": {
"CONTACT_SEARCH": {
@@ -578,8 +578,19 @@
"DESCRIPTION": "Select the labels you want to add to the selected contacts.",
"NO_LABELS_FOUND": "No labels available yet.",
"SELECTED_COUNT": "{count} selected",
"CLEAR_SELECTION": "Clear selection",
"SELECT_ALL": "Tümünü seç ({count})"
"CLEAR_SELECTION": "Seçimi temizle",
"SELECT_ALL": "Tümünü seç ({count})",
"DELETE_CONTACTS": "Sil",
"DELETE_SUCCESS": "Kişiler başarıyla silindi.",
"DELETE_FAILED": "Kişiler silinemedi.",
"DELETE_DIALOG": {
"TITLE": "Seçilen kişileri sil",
"SINGULAR_TITLE": "Seçilen kişiyi sil",
"DESCRIPTION": "Bu işlem, seçilen {count} kişileri kalıcı olarak silecektir. Bu eylem geri alınamaz.",
"SINGULAR_DESCRIPTION": "Bu işlem, seçilen kişiyi kalıcı olarak silecektir. Bu eylem geri alınamaz.",
"CONFIRM_MULTIPLE": "Kişileri sil",
"CONFIRM_SINGLE": "Kişiyi Sil"
}
},
"COMPOSE_NEW_CONVERSATION": {
"CONTACT_SEARCH": {
@@ -32,7 +32,7 @@
"LOADING_CONVERSATIONS": "Sohbetler Yükleniyor\n",
"CANNOT_REPLY": "Nedeniyle cevap veremezsiniz",
"24_HOURS_WINDOW": "24 saat mesaj penceresi kısıtlaması",
"API_HOURS_WINDOW": "Bu sohbete yalnızca {hours} saat içinde yanıt verebilirsiniz",
"API_HOURS_WINDOW": "Bu konuşmaya yalnızca {hours} saat içinde cevap 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.",
@@ -42,7 +42,7 @@
"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.",
"OLD_INSTAGRAM_INBOX_REPLY_BANNER": "Bu Instagram hesabı, yeni Instagram kanal gelen kutusuna taşındı. Tüm yeni mesajlar orada görünecektir. Artık bu konuşmadan mesaj gönderemeyeceksiniz.",
"REPLYING_TO": "Cevap veriyorsun:",
"REMOVE_SELECTION": "Seçimi Kaldır",
"DOWNLOAD": "İndir",
@@ -139,8 +139,8 @@
}
},
"DELETE_CONVERSATION": {
"TITLE": "#{conversationId} numaralı sohbeti sil",
"DESCRIPTION": "Bu sohbeti silmek istediğinizden emin misiniz?",
"TITLE": "#{conversationId} numaralı konuşmayı sil",
"DESCRIPTION": "Bu konuşmayı silmek istediğinizden emin misiniz?",
"CONFIRM": "Sil"
},
"CARD_CONTEXT_MENU": {
@@ -159,7 +159,7 @@
"ASSIGN_LABEL": "Etiket ata",
"AGENTS_LOADING": "Temsilciler Yükleniyor...",
"ASSIGN_TEAM": "Takım ata",
"DELETE": "Sohbeti sil",
"DELETE": "Konuşmayı 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ı",
@@ -244,8 +244,8 @@
"ASSIGN_LABEL_SUCCESFUL": "Etiket başarıyla atandı",
"ASSIGN_LABEL_FAILED": "Etiket ataması yapılamadı",
"CHANGE_TEAM": "Takım değişti",
"SUCCESS_DELETE_CONVERSATION": "Sohbet başarıyla silindi",
"FAIL_DELETE_CONVERSATION": "Sohbet silinemedi! Tekrar deneyin",
"SUCCESS_DELETE_CONVERSATION": "Konuşma başarıyla silindi",
"FAIL_DELETE_CONVERSATION": "Konuşma silinemedi! Tekrar deneyin",
"FILE_SIZE_LIMIT": "Dosya, {MAXIMUM_SUPPORTED_FILE_UPLOAD_SIZE} MB ek dosya sınırını aşıyor",
"MESSAGE_ERROR": "Bu mesaj gönderilemiyor, lütfen daha sonra tekrar deneyin",
"SENT_BY": "Tarafından gönderildi:",
@@ -1,8 +1,8 @@
{
"GENERAL_SETTINGS": {
"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.",
"CONVERSATION": "Konuşma limitini aştınız. Hacker planı yalnızca 500 konuşmaya izin vermektedir.",
"INBOXES": "Gelen kutusu limitini aştınız. Hacker planı yalnızca web sitesi canlı sohbetini desteklemektedir. E-posta, WhatsApp gibi ek gelen kutuları ücretli bir plan gerektirir.",
"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."
},
@@ -20,10 +20,10 @@
"BUTTON_TEXT": "Hesabınızı Silin",
"CONFIRM": {
"TITLE": "Hesabı Sil",
"MESSAGE": "Hesabınızı silmek geri alınamaz. Kalıcı olarak silmek istediğinizi onaylamak için aşağıya hesap adınızı girin.",
"MESSAGE": "Hesabınızı silmek geri alınamaz bir işlemdir. Kalıcı olarak silmek istediğinizi onaylamak için aşağıya hesap adınızı girin.",
"BUTTON_TEXT": "Sil",
"DISMISS": "İptal Et",
"PLACE_HOLDER": "{accountName} yazarak onaylayın"
"PLACE_HOLDER": "Onaylamak için lütfen {accountName} yazın"
},
"SUCCESS": "Hesap silinmek üzere işaretlendi",
"FAILURE": "Hesap silinemedi, tekrar deneyin!",
@@ -45,11 +45,11 @@
"NOTE": "API tabanlı bir entegrasyon oluşturuyorsanız bu kimlik gereklidir"
},
"AUTO_RESOLVE": {
"TITLE": "Sohbetleri otomatik çöz",
"NOTE": "Bu yapılandırma, belirli bir re etkinlik olmadığında sohbeti otomatik olarak çözmenizi sağlar.",
"TITLE": "Konuşmaları Otomatik Çöz",
"NOTE": "Bu yapılandırma, belirli bir hareketsizlik süresinden sonra konuşmayı otomatik olarak çözümlemenize olanak tanır.",
"DURATION": {
"LABEL": "Etkinliksizlik süresi",
"HELP": "Sohbetin otomatik olarak çözüleceği etkinliksizlik süresi",
"LABEL": "Hareketsizlik süresi",
"HELP": "Konuşmanın otomatik olarak çözümlenmesinden önceki hareketsizlik süresi",
"PLACEHOLDER": "30",
"ERROR": "Otomatik çözme süresi 10 dakika ile 999 gün arasında olmalıdır",
"API": {
@@ -59,8 +59,8 @@
},
"MESSAGE": {
"LABEL": "Özel otomatik çözüm mesajı",
"PLACEHOLDER": "Sohbet, 15 gün etkinlik olmadığı için sistem tarafından çözüldü olarak işaretlendi",
"HELP": "Sohbet otomatik olarak çözüldükten sonra müşteriye gönderilen mesaj"
"PLACEHOLDER": "Konuşma, 15 günlük hareketsizlik nedeniyle sistem tarafından çözümlendi olarak işaretlendi",
"HELP": "Konuşma otomatik olarak çözüldükten sonra müşteriye gönderilen mesaj"
},
"PREFERENCES": "Tercihler",
"LABEL": {
@@ -94,19 +94,19 @@
},
"AUTO_RESOLVE_IGNORE_WAITING": {
"LABEL": "Yanıtsız sohbetleri hariç tut",
"HELP": "Etkinleştirildiğinde, sistem hâlâ temsilci yanıtı bekleyen sohbetleri çözmeyi atlayacaktır."
"HELP": "Etkinleştirildiğinde, sistem hâlâ bir temsilcinin yanıtını bekleyen konuşmaları çözümlemeyi atlayacaktır."
},
"AUDIO_TRANSCRIPTION": {
"TITLE": "Sesli Mesajları Yazıya Döktür",
"NOTE": "Sohbetlerdeki sesli mesajları otomatik olarak yazıya dökün. Bir sesli mesaj gönderildiğinde veya alındığında bir metin dökümü oluşturun ve mesajın yanında gösterin.",
"TITLE": "Sesli Mesajları Metne Çevir",
"NOTE": "Konuşmalardaki sesli mesajları otomatik olarak metne çevirin. Bir sesli mesaj gönderildiğinde veya alındığında bir metin dökümü (transkript) oluşturun ve mesajın yanında görüntüleyin.",
"API": {
"SUCCESS": "Sesli mesaj transkripsiyon ayarı başarıyla güncellendi",
"ERROR": "Sesli yazıya dökme ayarı güncellenemedi"
"ERROR": "Ses transkripsiyon ayarı güncellenirken hata oluştu"
}
},
"AUTO_RESOLVE_DURATION": {
"LABEL": "Çözüm için etkinliksizlik süresi",
"HELP": "Bir sohbette etkinlik yoksa otomatik çözüm için süre",
"LABEL": "Çözümleme için hareketsizlik süresi",
"HELP": "Bir konuşmanın, hiçbir etkinlik olmazsa otomatik olarak çözümlenmesi gereken süre",
"PLACEHOLDER": "30",
"ERROR": "Otomatik çözme süresi 10 dakika ile 999 gün arasında olmalıdır",
"API": {
@@ -115,8 +115,8 @@
},
"UPDATE_BUTTON": "Güncelleme",
"MESSAGE_LABEL": "Özel çözüm mesajı",
"MESSAGE_PLACEHOLDER": "Sohbet, 15 gün etkinlik olmadığı için sistem tarafından çözüldü olarak işaretlendi",
"MESSAGE_HELP": "Bu mesaj, bir sohbet sistem tarafından etkinliksizlik nedeniyle otomatik olarak çözüldüğünde müşteriye gönderilir."
"MESSAGE_PLACEHOLDER": "Konuşma, 15 günlük hareketsizlik nedeniyle sistem tarafından çözümlendi olarak işaretlendi",
"MESSAGE_HELP": "Bu mesaj, bir konuşma hareketsizlik nedeniyle sistem tarafından otomatik olarak çözümlendiğinde müşteriye gönderilir."
},
"FEATURES": {
"INBOUND_EMAIL_ENABLED": "Hesabınız için e-posta ile iletişim devre dışı bırakıldı.",
@@ -51,10 +51,10 @@
"INSTAGRAM": {
"CONTINUE_WITH_INSTAGRAM": "Instagram ile devam et",
"CONNECT_YOUR_INSTAGRAM_PROFILE": "Instagram profilinizi bağlayın",
"HELP": "Bu Instagram hesabı yeni Instagram kanal gelen kutusuna taşındı. Artık bu gelen kutusundan Instagram mesajı gönderip alamayacaksınız ",
"HELP": "Instagram profilinizi kanal olarak eklemek için, 'Instagram ile Devam Et' seçeneğine tıklayarak Instagram Profilinizin kimliğini doğrulamanız gerekmektedir. ",
"ERROR_MESSAGE": "Instagram'a bağlanırken bir hata oluştu, lütfen tekrar deneyin",
"ERROR_AUTH": "Instagram'a bağlanırken bir hata oluştu, lütfen tekrar deneyin",
"NEW_INBOX_SUGGESTION": "Bu Instagram hesabı daha önce farklı bir gelen kutusuna bağlıydı ve şimdi buraya taşındı. Tüm yeni mesajlar burada görünecek. Eski gelen kutusu artık bu hesap için mesaj gönderip alamayacak.",
"NEW_INBOX_SUGGESTION": "Bu Instagram hesabı daha önce farklı bir gelen kutusuna bağlıydı ve şimdi buraya taşındı. Tüm yeni mesajlar burada görünecektir. Eski gelen kutusu, bu hesap için artık mesaj gönderemeyecek veya alamayacaktır.",
"DUPLICATE_INBOX_BANNER": "Bu Instagram hesabı yeni Instagram kanal gelen kutusuna taşındı. Artık bu gelen kutusundan Instagram mesajı gönderip alamayacaksınız."
},
"TWITTER": {
@@ -341,9 +341,9 @@
"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."
},
"SUBMIT_BUTTON": "Create Voice Channel",
"SUBMIT_BUTTON": "Ses Kanalı Oluştur",
"API": {
"ERROR_MESSAGE": "We were not able to create the voice channel"
"ERROR_MESSAGE": "Ses kanalı oluşturulamadı"
}
},
"API_CHANNEL": {
@@ -437,7 +437,7 @@
},
"FACEBOOK": {
"TITLE": "Facebook\n",
"DESCRIPTION": "Connect your Facebook page"
"DESCRIPTION": "Facebook sayfanızı bağlayın"
},
"WHATSAPP": {
"TITLE": "WhatsApp",
@@ -445,31 +445,31 @@
},
"EMAIL": {
"TITLE": "E-Posta",
"DESCRIPTION": "Connect with Gmail, Outlook, or other providers"
"DESCRIPTION": "Gmail, Outlook veya diğer sağlayıcılarla bağlantı kurun"
},
"SMS": {
"TITLE": "SMS",
"DESCRIPTION": "Integrate SMS channel with Twilio or bandwidth"
"DESCRIPTION": "SMS kanalını Twilio veya Bandwidth ile entegre edin"
},
"API": {
"TITLE": "API",
"DESCRIPTION": "Make a custom channel using our API"
"DESCRIPTION": "API'mizi kullanarak özel bir kanal oluşturun"
},
"TELEGRAM": {
"TITLE": "Telegram",
"DESCRIPTION": "Configure Telegram channel using Bot token"
"DESCRIPTION": "Bot token kullanarak Telegram kanalını yapılandırın"
},
"LINE": {
"TITLE": "Line",
"DESCRIPTION": "Integrate your Line channel"
"DESCRIPTION": "Line kanalınızı entegre edin"
},
"INSTAGRAM": {
"TITLE": "Instagram",
"DESCRIPTION": "Connect your instagram account"
"DESCRIPTION": "Instagram hesabınızı bağlayın"
},
"VOICE": {
"TITLE": "Ses",
"DESCRIPTION": "Integrate with Twilio Voice"
"DESCRIPTION": "Twilio Voice ile entegre edin"
}
}
},
@@ -526,9 +526,9 @@
"BUTTON_TEXT": "Beni oraya götür",
"MORE_SETTINGS": "Daha fazla ayar",
"WEBSITE_SUCCESS": "Bir web sitesi kanalı oluşturmayı başarıyla tamamladınız. Aşağıda gösterilen kodu kopyalayın ve web sitenize yapıştırın. Bir müşteri canlı sohbeti bir dahaki sefere kullandığında, konuşma otomatik olarak gelen kutunuzda görünecektir.",
"WHATSAPP_QR_INSTRUCTION": "Scan the QR code above to quickly test your WhatsApp inbox",
"MESSENGER_QR_INSTRUCTION": "Scan the QR code above to quickly test your Facebook Messenger inbox",
"TELEGRAM_QR_INSTRUCTION": "Scan the QR code above to quickly test your Telegram inbox"
"WHATSAPP_QR_INSTRUCTION": "WhatsApp gelen kutunuzu hızlıca test etmek için yukarıdaki QR kodu tarayın",
"MESSENGER_QR_INSTRUCTION": "Facebook Messenger gelen kutunuzu hızlıca test etmek için yukarıdaki QR kodu tarayın",
"TELEGRAM_QR_INSTRUCTION": "Telegram gelen kutunuzu hızlıca test etmek için yukarıdaki QR kodu tarayın"
},
"REAUTH": "Yeniden yetkilendir",
"VIEW": "Görünüm",
@@ -607,37 +607,37 @@
"BUSINESS_HOURS": "İş Saatleri",
"WIDGET_BUILDER": "Widget Oluşturucu",
"BOT_CONFIGURATION": "Bot Yapılandırma",
"ACCOUNT_HEALTH": "Account Health",
"ACCOUNT_HEALTH": "Hesap Sağlığı",
"CSAT": "CSAT"
},
"ACCOUNT_HEALTH": {
"TITLE": "Manage your WhatsApp account",
"DESCRIPTION": "Review your WhatsApp account status, messaging limits, and quality. Update settings or resolve issues if needed",
"GO_TO_SETTINGS": "Go to Meta Business Manager",
"NO_DATA": "Health data is not available",
"TITLE": "WhatsApp hesabınızı yönetin",
"DESCRIPTION": "WhatsApp hesap durumunuzu, mesajlaşma limitlerinizi ve kalitesini inceleyin. Gerekirse ayarları güncelleyin veya sorunları çözün",
"GO_TO_SETTINGS": "Meta Business Manager'a gidin",
"NO_DATA": "Sağlık verisi mevcut değil",
"FIELDS": {
"DISPLAY_PHONE_NUMBER": {
"LABEL": "Display phone number",
"TOOLTIP": "Phone number displayed to customers"
"LABEL": "Telefon numarasını göster",
"TOOLTIP": "Müşterilere gösterilen telefon numarası"
},
"VERIFIED_NAME": {
"LABEL": "Business name",
"TOOLTIP": "Business name verified by WhatsApp"
"LABEL": "İşletme adı",
"TOOLTIP": "WhatsApp tarafından onaylanmış işletme adı"
},
"DISPLAY_NAME_STATUS": {
"LABEL": "Display name status",
"TOOLTIP": "Status of your business name verification"
"LABEL": "Görünen adın durumu",
"TOOLTIP": "İşletme adı doğrulamanızın durumu"
},
"QUALITY_RATING": {
"LABEL": "Quality rating",
"TOOLTIP": "WhatsApp quality rating for your account"
"LABEL": "Kalite puanı",
"TOOLTIP": "Hesabınız için WhatsApp kalite puanı"
},
"MESSAGING_LIMIT_TIER": {
"LABEL": "Messaging limit tier",
"TOOLTIP": "Daily messaging limit for your account"
"LABEL": "Mesajlaşma limiti seviyesi",
"TOOLTIP": "Hesabınız için günlük mesajlaşma limiti"
},
"ACCOUNT_MODE": {
"LABEL": "Account mode",
"LABEL": "Hesap modu",
"TOOLTIP": "Current operating mode of your WhatsApp account"
}
},
@@ -784,7 +784,7 @@
},
"CSAT": {
"TITLE": "CSAT'yi etkinleştir",
"SUBTITLE": "Müşterilerin destek deneyimleri hakkında ne hissettiklerini anlamak için sohbetlerin sonunda otomatik olarak CSAT anketleri başlatın. Memnuniyet eğilimlerini takip edin ve zaman içinde iyileştirme alanlarını belirleyin.",
"SUBTITLE": "Müşterilerin destek deneyimleri hakkındaki düşüncelerini anlamak için konuşma sonunda CSAT anketlerini (Müşteri Memnuniyeti Anketleri) otomatik olarak tetikleyin. Memnuniyet eğilimlerini takip edin ve zaman içinde iyileştirme alanlarını belirleyin.",
"DISPLAY_TYPE": {
"LABEL": "Görüntüleme türü"
},
@@ -802,7 +802,7 @@
},
"SELECT_PLACEHOLDER": "etiketleri seç"
},
"NOTE": "Not: CSAT anketleri her sohbet için yalnızca bir kez gönderilir",
"NOTE": "Not: CSAT anketleri, konuşma başına yalnızca bir kez gönderilir",
"API": {
"SUCCESS_MESSAGE": "CSAT ayarları başarıyla güncellendi",
"ERROR_MESSAGE": "CSAT ayarları güncellenemedi. Lütfen daha sonra tekrar deneyin."
@@ -34,7 +34,7 @@
"SUBSCRIPTIONS": {
"LABEL": "Olaylar",
"EVENTS": {
"CONVERSATION_CREATED": "Görüşme Oluşturuldu",
"CONVERSATION_CREATED": "Konuşma Oluşturuldu",
"CONVERSATION_STATUS_CHANGED": "Görüşme Durumu Değişti",
"CONVERSATION_UPDATED": "Görüşme Güncellendi",
"MESSAGE_CREATED": "Mesaj Oluşturuldu",
@@ -42,8 +42,8 @@
"WEBWIDGET_TRIGGERED": "Kullanıcı tarafından canlı sohbet widget'ı açıldı",
"CONTACT_CREATED": "Kişi Oluşturuldu",
"CONTACT_UPDATED": "Kişi Güncellendi",
"CONVERSATION_TYPING_ON": "Sohbet Yazıyor Açık",
"CONVERSATION_TYPING_OFF": "Sohbet Yazıyor Kapalı"
"CONVERSATION_TYPING_ON": "Konuşmada Yazıyor Açık",
"CONVERSATION_TYPING_OFF": "Konuşmada Yazıyor Kapalı"
}
},
"END_POINT": {
@@ -327,7 +327,7 @@
},
"CTA": {
"TITLE": "Linear'a bağlan",
"AGENT_DESCRIPTION": "Doğrusal çalışma alanı bağlı değil. Bu entegrasyonu kullanmak için yöneticinizden bir çalışma alanı bağlamasını isteyin.",
"AGENT_DESCRIPTION": "Linear çalışma alanı bağlı değil. Bu entegrasyonu kullanmak için yöneticinizden bir çalışma alanı bağlamasını isteyin.",
"DESCRIPTION": "Linear çalışma alanı bağlı değil. Bu entegrasyonu kullanmak için çalışma alanınızı bağlamak üzere aşağıdaki düğmeye tıklayın.",
"BUTTON_TEXT": "Linear çalışma alanını bağlayın"
}
@@ -348,7 +348,7 @@
"TITLE": "Copilot",
"TRY_THESE_PROMPTS": "Try these prompts",
"PANEL_TITLE": "Copilot ile başlayın",
"KICK_OFF_MESSAGE": "Hızlı bir özet mi lazım, geçmiş sohbetleri mi kontrol etmek istiyorsunuz ya da daha iyi bir yanıt mı yazmak istiyorsunuz? Copilot işleri hızlandırmak için burada.",
"KICK_OFF_MESSAGE": "Hızlı bir özet mi gerekiyor, geçmiş konuşmaları mı kontrol etmek istiyorsunuz, yoksa daha iyi bir yanıt mı tasarlamak istiyorsunuz? Copilot işleri hızlandırmak için burada.",
"SEND_MESSAGE": "Mesajı Gönder...",
"EMPTY_MESSAGE": "Yanıt oluşturulurken bir hata oluştu. Lütfen tekrar deneyin.",
"LOADER": "Captain is thinking",
@@ -371,8 +371,8 @@
"CONTENT": "Sohbetin müşterinin ihtiyaçlarını ne kadar karşıladığını gözden geçirin. Ton, açıklık ve etkililik açısından 5 üzerinden bir puan verin."
},
"HIGH_PRIORITY": {
"LABEL": "Yüksek öncelikli sohbetler",
"CONTENT": "Tüm yüksek öncelikli açık sohbetlerin bir özetini verin. Sohbet kimliği, müşteri adı (varsa), son mesaj içeriği ve atanan temsilciyi ekleyin. Gerekirse duruma göre gruplayın."
"LABEL": "Yüksek öncelikli konuşmalar",
"CONTENT": "Tüm yüksek öncelikli açık konuşmaların bir özetini verin. Konuşma kimliği, müşteri adı (varsa), son mesaj içeriği ve atanan temsilciyi ekleyin. Gerekirse duruma göre gruplayın."
},
"LIST_CONTACTS": {
"LABEL": "Kişileri listele",
@@ -383,7 +383,7 @@
"PLAYGROUND": {
"USER": "Sen",
"ASSISTANT": "Assistant",
"MESSAGE_PLACEHOLDER": "Mesajınız...",
"MESSAGE_PLACEHOLDER": "Mesajınızı yazın...",
"HEADER": "Oyun Alanı",
"DESCRIPTION": "Bu oyun alanını asistanınıza mesaj göndermek ve yanıtlarının doğru, hızlı ve beklediğiniz tonda olup olmadığını kontrol etmek için kullanın.",
"CREDIT_NOTE": "Buradan gönderilen mesajlar Captain kredilerinize sayılacaktır."
@@ -194,7 +194,7 @@
},
"LABEL_REPORTS": {
"HEADER": "Etiketler Genel Bakış",
"DESCRIPTION": "Konuşmalar, yanıt süreleri, çözüm süreleri ve çözülmüş vakalar gibi temel ölçümlerle etiket performansını takip edin. Ayrıntılı içgörüler için bir etiket adına tıklayın.",
"DESCRIPTION": "Etiket performansını; konuşmalar, yanıt süreleri, çözüm süreleri ve çözümlenen vakalar dahil olmak üzere temel metriklerle takip edin. Ayrıntılı analizler için bir etiket adına tıklayın.",
"LOADING_CHART": "Grafik verileri yükleniyor...",
"NO_ENOUGH_DATA": "Rapor oluşturmak için yeterli veri yok, Lütfen daha sonra tekrar deneyin.",
"DOWNLOAD_LABEL_REPORTS": "Etiket raporlarını indir",
@@ -579,7 +579,18 @@
"NO_LABELS_FOUND": "No labels available yet.",
"SELECTED_COUNT": "{count} selected",
"CLEAR_SELECTION": "Clear selection",
"SELECT_ALL": "Select all ({count})"
"SELECT_ALL": "Select all ({count})",
"DELETE_CONTACTS": "Видалити",
"DELETE_SUCCESS": "Contacts deleted successfully.",
"DELETE_FAILED": "Failed to delete contacts.",
"DELETE_DIALOG": {
"TITLE": "Delete selected contacts",
"SINGULAR_TITLE": "Delete selected contact",
"DESCRIPTION": "This will permanently delete {count} selected contacts. This action cannot be undone.",
"SINGULAR_DESCRIPTION": "This will permanently delete the selected contact. This action cannot be undone.",
"CONFIRM_MULTIPLE": "Delete contacts",
"CONFIRM_SINGLE": "Видалити контакт"
}
},
"COMPOSE_NEW_CONVERSATION": {
"CONTACT_SEARCH": {
@@ -579,7 +579,18 @@
"NO_LABELS_FOUND": "No labels available yet.",
"SELECTED_COUNT": "{count} selected",
"CLEAR_SELECTION": "Clear selection",
"SELECT_ALL": "Select all ({count})"
"SELECT_ALL": "Select all ({count})",
"DELETE_CONTACTS": "حذف کریں۔",
"DELETE_SUCCESS": "Contacts deleted successfully.",
"DELETE_FAILED": "Failed to delete contacts.",
"DELETE_DIALOG": {
"TITLE": "Delete selected contacts",
"SINGULAR_TITLE": "Delete selected contact",
"DESCRIPTION": "This will permanently delete {count} selected contacts. This action cannot be undone.",
"SINGULAR_DESCRIPTION": "This will permanently delete the selected contact. This action cannot be undone.",
"CONFIRM_MULTIPLE": "Delete contacts",
"CONFIRM_SINGLE": "کانٹیکٹ حذف کریں۔"
}
},
"COMPOSE_NEW_CONVERSATION": {
"CONTACT_SEARCH": {
@@ -579,7 +579,18 @@
"NO_LABELS_FOUND": "No labels available yet.",
"SELECTED_COUNT": "{count} selected",
"CLEAR_SELECTION": "Clear selection",
"SELECT_ALL": "Select all ({count})"
"SELECT_ALL": "Select all ({count})",
"DELETE_CONTACTS": "Delete",
"DELETE_SUCCESS": "Contacts deleted successfully.",
"DELETE_FAILED": "Failed to delete contacts.",
"DELETE_DIALOG": {
"TITLE": "Delete selected contacts",
"SINGULAR_TITLE": "Delete selected contact",
"DESCRIPTION": "This will permanently delete {count} selected contacts. This action cannot be undone.",
"SINGULAR_DESCRIPTION": "This will permanently delete the selected contact. This action cannot be undone.",
"CONFIRM_MULTIPLE": "Delete contacts",
"CONFIRM_SINGLE": "Delete contact"
}
},
"COMPOSE_NEW_CONVERSATION": {
"CONTACT_SEARCH": {
@@ -579,7 +579,18 @@
"NO_LABELS_FOUND": "No labels available yet.",
"SELECTED_COUNT": "{count} selected",
"CLEAR_SELECTION": "Clear selection",
"SELECT_ALL": "Select all ({count})"
"SELECT_ALL": "Select all ({count})",
"DELETE_CONTACTS": "Xoá",
"DELETE_SUCCESS": "Contacts deleted successfully.",
"DELETE_FAILED": "Failed to delete contacts.",
"DELETE_DIALOG": {
"TITLE": "Delete selected contacts",
"SINGULAR_TITLE": "Delete selected contact",
"DESCRIPTION": "This will permanently delete {count} selected contacts. This action cannot be undone.",
"SINGULAR_DESCRIPTION": "This will permanently delete the selected contact. This action cannot be undone.",
"CONFIRM_MULTIPLE": "Delete contacts",
"CONFIRM_SINGLE": "Xoá liên hệ"
}
},
"COMPOSE_NEW_CONVERSATION": {
"CONTACT_SEARCH": {
@@ -579,7 +579,18 @@
"NO_LABELS_FOUND": "No labels available yet.",
"SELECTED_COUNT": "{count} 已选择",
"CLEAR_SELECTION": "Clear selection",
"SELECT_ALL": "全选 ({count})"
"SELECT_ALL": "全选 ({count})",
"DELETE_CONTACTS": "删除",
"DELETE_SUCCESS": "Contacts deleted successfully.",
"DELETE_FAILED": "Failed to delete contacts.",
"DELETE_DIALOG": {
"TITLE": "Delete selected contacts",
"SINGULAR_TITLE": "Delete selected contact",
"DESCRIPTION": "This will permanently delete {count} selected contacts. This action cannot be undone.",
"SINGULAR_DESCRIPTION": "This will permanently delete the selected contact. This action cannot be undone.",
"CONFIRM_MULTIPLE": "Delete contacts",
"CONFIRM_SINGLE": "删除联系人"
}
},
"COMPOSE_NEW_CONVERSATION": {
"CONTACT_SEARCH": {
@@ -579,7 +579,18 @@
"NO_LABELS_FOUND": "No labels available yet.",
"SELECTED_COUNT": "{count} selected",
"CLEAR_SELECTION": "Clear selection",
"SELECT_ALL": "Select all ({count})"
"SELECT_ALL": "Select all ({count})",
"DELETE_CONTACTS": "刪除",
"DELETE_SUCCESS": "Contacts deleted successfully.",
"DELETE_FAILED": "Failed to delete contacts.",
"DELETE_DIALOG": {
"TITLE": "Delete selected contacts",
"SINGULAR_TITLE": "Delete selected contact",
"DESCRIPTION": "This will permanently delete {count} selected contacts. This action cannot be undone.",
"SINGULAR_DESCRIPTION": "This will permanently delete the selected contact. This action cannot be undone.",
"CONFIRM_MULTIPLE": "Delete contacts",
"CONFIRM_SINGLE": "刪除聯絡人"
}
},
"COMPOSE_NEW_CONVERSATION": {
"CONTACT_SEARCH": {
@@ -10,14 +10,14 @@ const assistantId = computed(() => Number(route.params.assistantId));
<template>
<PageLayout
:header-title="$t('CAPTAIN.PLAYGROUND.HEADER')"
show-assistant-switcher
:show-pagination-footer="false"
:show-know-more="false"
class="h-full"
>
<template #body>
<div class="flex flex-col h-full">
<AssistantPlayground :assistant-id="assistantId" />
<AssistantPlayground :assistant-id="assistantId" class="bg-n-solid-1" />
</div>
</template>
</PageLayout>
@@ -104,7 +104,6 @@ const handleDeleteSuccess = () => {
<template>
<PageLayout
:header-title="$t('CAPTAIN.ASSISTANTS.SETTINGS.HEADER')"
:is-fetching="isFetching"
:show-pagination-footer="false"
:show-know-more="false"
@@ -195,7 +195,10 @@ const navigateToPendingFAQs = () => {
onMounted(() => {
initializeFromURL();
store.dispatch('captainResponses/fetchPendingCount', selectedAssistantId);
store.dispatch(
'captainResponses/fetchPendingCount',
selectedAssistantId.value
);
});
</script>
@@ -55,6 +55,7 @@ export default {
data() {
return {
url: this.value.url || '',
name: this.value.name || '',
subscriptions: this.value.subscriptions || [],
supportedWebhookEvents: SUPPORTED_WEBHOOK_EVENTS,
};
@@ -68,11 +69,15 @@ export default {
}
);
},
webhookNameInputPlaceholder() {
return this.$t('INTEGRATION_SETTINGS.WEBHOOK.FORM.NAME.PLACEHOLDER');
},
},
methods: {
onSubmit() {
this.$emit('submit', {
url: this.url,
name: this.name,
subscriptions: this.subscriptions,
});
},
@@ -97,6 +102,15 @@ export default {
{{ $t('INTEGRATION_SETTINGS.WEBHOOK.FORM.END_POINT.ERROR') }}
</span>
</label>
<label>
{{ $t('INTEGRATION_SETTINGS.WEBHOOK.FORM.NAME.LABEL') }}
<input
v-model="name"
type="text"
name="name"
:placeholder="webhookNameInputPlaceholder"
/>
</label>
<label :class="{ error: v$.url.$error }" class="mb-2">
{{ $t('INTEGRATION_SETTINGS.WEBHOOK.FORM.SUBSCRIPTIONS.LABEL') }}
</label>
@@ -37,8 +37,16 @@ const subscribedEvents = computed(() => {
<template>
<tr>
<td class="py-4 ltr:pr-4 rtl:pl-4">
<div class="font-medium break-words text-n-slate-12">
{{ webhook.url }}
<div class="flex gap-2 font-medium break-words text-n-slate-12">
<template v-if="webhook.name">
{{ webhook.name }}
<span class="text-n-slate-11">
{{ webhook.url }}
</span>
</template>
<template v-else>
{{ webhook.url }}
</template>
</div>
<div class="block mt-1 text-sm text-n-slate-11">
<span class="font-medium">
@@ -119,7 +119,7 @@ const tooltip = useHeatmapTooltip();
<!-- eslint-disable vue/no-static-inline-styles -->
<template>
<div
class="grid relative w-full gap-x-4 gap-y-2.5 overflow-y-scroll md:overflow-visible grid-cols-[80px_1fr] min-h-72"
class="grid relative w-full gap-x-4 gap-y-2.5 overflow-y-scroll md:overflow-visible grid-cols-[80px_1fr]"
>
<template v-if="isLoading">
<div class="grid gap-[5px] flex-shrink-0">
@@ -1,15 +1,18 @@
<script setup>
import { onMounted, ref, computed } from 'vue';
import { onMounted, ref, computed, watch } from 'vue';
import { useToggle } from '@vueuse/core';
import MetricCard from '../overview/MetricCard.vue';
import BaseHeatmap from './BaseHeatmap.vue';
import HeatmapDateRangeSelector from './HeatmapDateRangeSelector.vue';
import { useStore, useMapGetter } from 'dashboard/composables/store';
import { useLiveRefresh } from 'dashboard/composables/useLiveRefresh';
import differenceInCalendarDays from 'date-fns/differenceInCalendarDays';
import endOfDay from 'date-fns/endOfDay';
import format from 'date-fns/format';
import getUnixTime from 'date-fns/getUnixTime';
import startOfDay from 'date-fns/startOfDay';
import startOfMonth from 'date-fns/startOfMonth';
import subDays from 'date-fns/subDays';
import format from 'date-fns/format';
import DropdownMenu from 'dashboard/components-next/dropdown-menu/DropdownMenu.vue';
import Button from 'dashboard/components-next/button/Button.vue';
import { useI18n } from 'vue-i18n';
@@ -57,27 +60,33 @@ const uiFlags = useMapGetter('getOverviewUIFlags');
const heatmapData = useMapGetter(props.storeGetter);
const inboxes = useMapGetter('inboxes/getInboxes');
const menuItems = [
{
label: t('REPORT.DATE_RANGE_OPTIONS.LAST_7_DAYS'),
value: 6,
},
{
label: t('REPORT.DATE_RANGE_OPTIONS.LAST_14_DAYS'),
value: 13,
},
{
label: t('REPORT.DATE_RANGE_OPTIONS.LAST_30_DAYS'),
value: 29,
},
];
const selectedDays = ref(6);
const selectedFrom = ref(null);
const selectedTo = ref(null);
const selectedDaysBefore = ref(null);
const selectedInbox = ref(null);
const isMonthFilter = ref(false);
const currentMonthOffset = ref(0);
const selectedDayFilter = computed(() =>
menuItems.find(menuItem => menuItem.value === selectedDays.value)
);
const selectedRange = computed(() => {
if (!selectedFrom.value || !selectedTo.value) {
return null;
}
return {
from: selectedFrom.value,
to: selectedTo.value,
};
});
const numberOfRows = computed(() => {
if (!selectedRange.value) {
return 0;
}
const dateDifference = differenceInCalendarDays(
selectedRange.value.to,
selectedRange.value.from
);
return dateDifference + 1;
});
const inboxMenuItems = computed(() => {
return [
@@ -105,13 +114,42 @@ const selectedInboxFilter = computed(() => {
const isLoading = computed(() => uiFlags.value[props.uiFlagKey]);
// Keeps relative presets (last 7 days / this month) aligned with "now" during live refreshes.
const resolveActiveRange = () => {
if (isMonthFilter.value && currentMonthOffset.value === 0) {
const now = new Date();
const monthStart = startOfMonth(now);
return {
from: startOfDay(monthStart),
to: endOfDay(now),
};
}
if (!isMonthFilter.value && selectedDaysBefore.value !== null) {
const to = endOfDay(new Date());
return {
from: startOfDay(subDays(to, Number(selectedDaysBefore.value))),
to,
};
}
return selectedRange.value;
};
const downloadHeatmapData = () => {
const to = endOfDay(new Date());
const range = resolveActiveRange();
if (!range) {
return;
}
const { to } = range;
const shouldUseBackendDownload =
!isMonthFilter.value && !selectedInbox.value && props.downloadAction;
// If no inbox is selected and download action exists, use backend endpoint
if (!selectedInbox.value && props.downloadAction) {
if (shouldUseBackendDownload) {
store.dispatch(props.downloadAction, {
daysBefore: selectedDays.value,
daysBefore: selectedDaysBefore.value,
to: getUnixTime(to),
});
return;
@@ -150,7 +188,6 @@ const downloadHeatmapData = () => {
downloadCsvFile(fileName, csvContent);
};
const [showDropdown, toggleDropdown] = useToggle();
const [showInboxDropdown, toggleInboxDropdown] = useToggle();
const fetchHeatmapData = () => {
@@ -158,8 +195,12 @@ const fetchHeatmapData = () => {
return;
}
let to = endOfDay(new Date());
let from = startOfDay(subDays(to, Number(selectedDays.value)));
const range = resolveActiveRange();
if (!range) {
return;
}
const { from, to } = range;
const params = {
metric: props.metric,
@@ -178,25 +219,43 @@ const fetchHeatmapData = () => {
store.dispatch(props.storeAction, params);
};
const handleAction = ({ value }) => {
toggleDropdown(false);
selectedDays.value = value;
fetchHeatmapData();
};
const handleInboxAction = ({ value }) => {
toggleInboxDropdown(false);
selectedInbox.value = value
? inboxes.value.find(inbox => inbox.id === value)
: null;
fetchHeatmapData();
};
const { startRefetching } = useLiveRefresh(fetchHeatmapData);
const handleRangeTypeChange = type => {
isMonthFilter.value = type === 'month';
};
const handleMonthOffsetChange = offset => {
currentMonthOffset.value = offset;
};
watch(
() => [selectedFrom.value, selectedTo.value],
([from, to]) => {
if (from && to) {
fetchHeatmapData();
}
}
);
watch(
() => selectedInbox.value,
() => {
if (selectedRange.value) {
fetchHeatmapData();
}
}
);
onMounted(() => {
store.dispatch('inboxes/get');
fetchHeatmapData();
startRefetching();
});
</script>
@@ -205,25 +264,13 @@ onMounted(() => {
<div class="flex flex-row flex-wrap max-w-full">
<MetricCard :header="title">
<template #control>
<div
v-on-clickaway="() => toggleDropdown(false)"
class="relative flex items-center group"
>
<Button
sm
slate
faded
:label="selectedDayFilter.label"
class="rounded-md group-hover:bg-n-alpha-2"
@click="toggleDropdown()"
/>
<DropdownMenu
v-if="showDropdown"
:menu-items="menuItems"
class="mt-1 ltr:right-0 rtl:left-0 xl:ltr:right-0 xl:rtl:left-0 top-full"
@action="handleAction($event)"
/>
</div>
<HeatmapDateRangeSelector
v-model:from="selectedFrom"
v-model:to="selectedTo"
v-model:days-num="selectedDaysBefore"
@range-type-change="handleRangeTypeChange"
@month-offset-change="handleMonthOffsetChange"
/>
<div
v-on-clickaway="() => toggleInboxDropdown(false)"
class="relative flex items-center group"
@@ -241,22 +288,23 @@ onMounted(() => {
:menu-items="inboxMenuItems"
show-search
:search-placeholder="t('INBOX_REPORTS.SEARCH_INBOX')"
class="mt-1 ltr:right-0 rtl:left-0 xl:ltr:right-0 xl:rtl:left-0 top-full min-w-[200px]"
class="mt-1 ltr:right-0 rtl:left-0 xl:ltr:right-0 xl:rtl:left-0 top-full !min-w-56 max-w-56 max-h-96 overflow-y-auto"
@action="handleInboxAction($event)"
/>
</div>
<Button
v-tooltip="t('OVERVIEW_REPORTS.CONVERSATION_HEATMAP.DOWNLOAD_REPORT')"
sm
slate
faded
:label="t('OVERVIEW_REPORTS.CONVERSATION_HEATMAP.DOWNLOAD_REPORT')"
icon="i-lucide-download"
class="rounded-md group-hover:bg-n-alpha-2"
@click="downloadHeatmapData"
/>
</template>
<BaseHeatmap
:heatmap-data="heatmapData"
:number-of-rows="selectedDays + 1"
:number-of-rows="numberOfRows"
:is-loading="isLoading"
:color-scheme="colorScheme"
/>
@@ -0,0 +1,211 @@
<script setup>
import { computed, ref, watch, defineModel } from 'vue';
import { useToggle } from '@vueuse/core';
import { useI18n } from 'vue-i18n';
import addMonths from 'date-fns/addMonths';
import differenceInCalendarDays from 'date-fns/differenceInCalendarDays';
import endOfDay from 'date-fns/endOfDay';
import endOfMonth from 'date-fns/endOfMonth';
import startOfDay from 'date-fns/startOfDay';
import startOfMonth from 'date-fns/startOfMonth';
import subDays from 'date-fns/subDays';
import { vOnClickOutside } from '@vueuse/components';
import Button from 'dashboard/components-next/button/Button.vue';
import DropdownMenu from 'dashboard/components-next/dropdown-menu/DropdownMenu.vue';
const emit = defineEmits(['rangeTypeChange', 'monthOffsetChange']);
const fromModel = defineModel('from', { type: Date, default: null });
const toModel = defineModel('to', { type: Date, default: null });
const daysNumModel = defineModel('daysNum', { type: Number, default: null });
const { t, locale } = useI18n();
const DATE_FILTER_TYPES = {
DAY: 'day',
MONTH: 'month',
};
const DATE_FILTER_ACTION = 'select_date_range';
const dayMenuItemConfigs = computed(() => [
{
label: t('REPORT.DATE_RANGE_OPTIONS.LAST_7_DAYS'),
value: 'last_7_days',
action: DATE_FILTER_ACTION,
type: DATE_FILTER_TYPES.DAY,
daysBefore: 6,
},
{
label: t('REPORT.DATE_RANGE_OPTIONS.LAST_14_DAYS'),
value: 'last_14_days',
action: DATE_FILTER_ACTION,
type: DATE_FILTER_TYPES.DAY,
daysBefore: 13,
},
{
label: t('REPORT.DATE_RANGE_OPTIONS.LAST_30_DAYS'),
value: 'last_30_days',
action: DATE_FILTER_ACTION,
type: DATE_FILTER_TYPES.DAY,
daysBefore: 29,
},
]);
const resolvedLocale = computed(
() =>
locale.value ||
(typeof navigator !== 'undefined' ? navigator.language : 'en')
);
const monthFormatter = computed(
() =>
new Intl.DateTimeFormat(resolvedLocale.value, {
month: 'long',
year: 'numeric',
})
);
const monthMenuItemConfigs = computed(() => {
const now = new Date();
const offsets = [0, -1, -2];
return offsets.map(offset => ({
label:
offset === 0
? t('REPORT.DATE_RANGE_OPTIONS.THIS_MONTH')
: monthFormatter.value.format(addMonths(now, offset)),
value: offset === 0 ? 'this_month' : `month_${offset}`,
action: DATE_FILTER_ACTION,
type: DATE_FILTER_TYPES.MONTH,
monthOffset: offset,
}));
});
const selectedDateRangeValue = ref('');
const [showDropdown, toggleDropdown] = useToggle();
const monthOffset = ref(0);
const menuItems = computed(() => {
const selectedValue = selectedDateRangeValue.value;
return [...dayMenuItemConfigs.value, ...monthMenuItemConfigs.value].map(
config => ({
...config,
isSelected: selectedValue === config.value,
})
);
});
selectedDateRangeValue.value = menuItems.value[0]?.value || '';
const menuSections = computed(() => {
const dayItems = menuItems.value.filter(
item => item.type === DATE_FILTER_TYPES.DAY
);
const monthItems = menuItems.value.filter(
item => item.type === DATE_FILTER_TYPES.MONTH
);
return [{ items: dayItems }, { items: monthItems }].filter(
section => section.items.length > 0
);
});
const selectedConfig = computed(
() =>
menuItems.value.find(
menuItem => menuItem.value === selectedDateRangeValue.value
) || menuItems.value[0]
);
const selectedLabel = computed(() => {
const selectedItem = menuItems.value.find(
item => item.value === selectedDateRangeValue.value
);
return selectedItem?.label || '';
});
const computeRange = config => {
if (!config) {
return null;
}
if (config.type === DATE_FILTER_TYPES.MONTH) {
const now = new Date();
const baseMonthStart = startOfMonth(addMonths(now, monthOffset.value));
const from = startOfDay(baseMonthStart);
const isCurrentMonth =
config.value === 'this_month' && monthOffset.value === 0;
const to = isCurrentMonth
? endOfDay(now)
: endOfDay(endOfMonth(baseMonthStart));
const daysBefore = differenceInCalendarDays(to, from);
return { from, to, daysBefore };
}
const to = endOfDay(new Date());
const from = startOfDay(subDays(to, Number(config.daysBefore)));
return { from, to, daysBefore: Number(config.daysBefore) };
};
const applySelection = config => {
if (!config) return;
if (config.type === DATE_FILTER_TYPES.MONTH) {
monthOffset.value = config.monthOffset || 0;
} else {
monthOffset.value = 0;
}
const range = computeRange(config);
if (!range) return;
const { from, to, daysBefore } = range;
fromModel.value = from;
toModel.value = to;
daysNumModel.value = daysBefore;
emit('rangeTypeChange', config.type);
emit('monthOffsetChange', monthOffset.value);
};
const handleAction = ({ action, value }) => {
toggleDropdown(false);
if (action !== DATE_FILTER_ACTION) {
return;
}
selectedDateRangeValue.value = value;
};
watch(
() => selectedConfig.value,
config => {
applySelection(config);
},
{ immediate: true }
);
</script>
<template>
<div
v-on-click-outside="() => toggleDropdown(false)"
class="relative flex items-center group"
>
<Button
sm
slate
faded
:label="selectedLabel"
class="rounded-md group-hover:bg-n-alpha-2"
@click="toggleDropdown()"
/>
<DropdownMenu
v-if="showDropdown"
:menu-items="menuItems"
:menu-sections="menuSections"
class="mt-1 ltr:right-0 rtl:left-0 xl:ltr:right-0 xl:rtl:left-0 top-full"
@action="handleAction($event)"
/>
</div>
</template>
+18 -1
View File
@@ -8,7 +8,7 @@ class Webhooks::InstagramEventsJob < MutexApplicationJob
def perform(entries)
@entries = entries
key = format(::Redis::Alfred::IG_MESSAGE_MUTEX, sender_id: sender_id, ig_account_id: ig_account_id)
key = format(::Redis::Alfred::IG_MESSAGE_MUTEX, sender_id: contact_instagram_id, ig_account_id: ig_account_id)
with_lock(key) do
process_entries(entries)
end
@@ -77,6 +77,23 @@ class Webhooks::InstagramEventsJob < MutexApplicationJob
@entries&.first&.dig(:id)
end
def contact_instagram_id
entry = @entries&.first
return nil unless entry
# Handle both messaging and standby arrays
messaging = (entry[:messaging].presence || entry[:standby] || []).first
return nil unless messaging
# For echo messages (outgoing from our account), use recipient's ID (the contact)
# For incoming messages (from contact), use sender's ID (the contact)
if messaging.dig(:message, :is_echo)
messaging.dig(:recipient, :id)
else
messaging.dig(:sender, :id)
end
end
def sender_id
@entries&.dig(0, :messaging, 0, :sender, :id)
end
+7 -24
View File
@@ -4,38 +4,21 @@ class ApplicationMailbox < ActionMailbox::Base
# Last part is the regex for the UUID
# Eg: email should be something like : reply+6bdc3f4d-0bec-4515-a284-5d916fdde489@domain.com
REPLY_EMAIL_UUID_PATTERN = /^reply\+([0-9a-f]{8}\b-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-\b[0-9a-f]{12})$/i
CONVERSATION_MESSAGE_ID_PATTERN = %r{conversation/([a-zA-Z0-9-]*?)/messages/(\d+?)@(\w+\.\w+)}
# routes as a reply to existing conversations
# Route all emails to verified channels to the unified reply mailbox
# The ConversationFinder will determine if it's a reply or new conversation
routing(
->(inbound_mail) { valid_to_address?(inbound_mail) && (reply_uuid_mail?(inbound_mail) || in_reply_to_mail?(inbound_mail)) } => :reply
)
# routes as a new conversation in email channel
routing(
->(inbound_mail) { valid_to_address?(inbound_mail) && EmailChannelFinder.new(inbound_mail.mail).perform.present? } => :support
lambda { |inbound_mail|
valid_to_address?(inbound_mail) &&
(reply_uuid_mail?(inbound_mail) || EmailChannelFinder.new(inbound_mail.mail).perform.present?)
} => :reply
)
# catchall
routing(all: :default)
class << self
# checks if follow this pattern then send it to reply_mailbox
# <account/#{@account.id}/conversation/#{@conversation.uuid}@#{@account.inbound_email_domain}>
def in_reply_to_mail?(inbound_mail)
in_reply_to = inbound_mail.mail.in_reply_to
in_reply_to.present? && (
in_reply_to_matches?(in_reply_to) || Message.exists?(source_id: in_reply_to)
)
end
def in_reply_to_matches?(in_reply_to)
Array.wrap(in_reply_to).any? { it.match?(CONVERSATION_MESSAGE_ID_PATTERN) }
end
# checks if follow this pattern send it to reply_mailbox
# reply+<conversation-uuid>@<mailer-domain.com>
# checks if follows this pattern: reply+<conversation-uuid>@<mailer-domain.com>
def reply_uuid_mail?(inbound_mail)
inbound_mail.mail.to&.any? do |email|
conversation_uuid = email.split('@')[0]
+19 -4
View File
@@ -3,6 +3,8 @@ class Imap::ImapMailbox
include IncomingEmailValidityHelper
attr_accessor :channel, :account, :inbox, :conversation, :processed_mail
FALLBACK_CONVERSATION_PATTERN = %r{account/(\d+)/conversation/([a-zA-Z0-9-]+)@}
def process(mail, channel)
@inbound_mail = mail
@channel = channel
@@ -49,19 +51,32 @@ class Imap::ImapMailbox
end
def find_conversation_by_reference_ids
return if @inbound_mail.references.blank? && in_reply_to.present?
return if @inbound_mail.references.blank?
message = find_message_by_references
if message.present?
conversation = @inbox.conversations.find_by(id: message.conversation_id)
return conversation if conversation.present?
end
return if message.nil?
@inbox.conversations.find(message.conversation_id)
# FALLBACK_PATTERN use to find a conversation that is started by an agent (no incoming message yet)
conversation_id = find_conversation_by_references
@inbox.conversations.find_by(uuid: conversation_id) if conversation_id.present?
end
def in_reply_to
@processed_mail.in_reply_to
end
def find_conversation_by_references
references = Array.wrap(@inbound_mail.references)
references.each do |message_id|
match = FALLBACK_CONVERSATION_PATTERN.match(message_id)
return match[2] if match.present?
end
end
def find_message_by_references
message_to_return = nil
+23 -73
View File
@@ -1,88 +1,38 @@
class ReplyMailbox < ApplicationMailbox
attr_accessor :conversation_uuid, :processed_mail
attr_accessor :conversation, :processed_mail
# Last part is the regex for the UUID
# Eg: email should be something like : reply+6bdc3f4d-0bec-4515-a284-5d916fdde489@domain.com
EMAIL_PART_PATTERN = /^reply\+([0-9a-f]{8}\b-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-\b[0-9a-f]{12})$/i
before_processing :conversation_uuid_from_to_address,
:find_relative_conversation
before_processing :find_conversation
def process
return if @conversation.blank?
# Return early if no conversation was found (e.g., notification emails, suspended accounts)
return unless @conversation
decorate_mail
create_message
add_attachments_to_message
# Wrap everything in a transaction to ensure atomicity
# This prevents orphan conversations if message/attachment creation fails
# and ensures idempotency on job retry (conversation won't be duplicated)
ActiveRecord::Base.transaction do
persist_conversation_if_needed
decorate_mail
create_message
add_attachments_to_message
end
end
private
def find_relative_conversation
if @conversation_uuid
find_conversation_with_uuid
elsif mail.in_reply_to.present?
find_conversation_with_in_reply_to
end
def find_conversation
@conversation = Mailbox::ConversationFinder.new(mail).find
# Log when email is rejected
Rails.logger.info "Email #{mail.message_id} rejected - no conversation found" unless @conversation
end
def conversation_uuid_from_to_address
@mail = MailPresenter.new(mail)
def persist_conversation_if_needed
# Save the conversation if it's a new record (from NewConversationStrategy)
# We persist here instead of in the strategy to maintain transaction integrity
return unless @conversation.new_record?
return if @mail.mail_receiver.blank?
@mail.mail_receiver.each do |email|
username = email.split('@')[0]
match_result = username.match(ApplicationMailbox::REPLY_EMAIL_UUID_PATTERN)
if match_result
@conversation_uuid = match_result.captures
break
end
end
@conversation_uuid
end
# find conversation uuid from below pattern
# reply+<conversation-uuid>@<mailer-domain.com>
def find_conversation_with_uuid
@conversation = Conversation.find_by(uuid: conversation_uuid)
validate_resource @conversation
end
def find_conversation_by_uuid(match_result)
@conversation_uuid = match_result.captures[0]
find_conversation_with_uuid
end
def find_conversation_by_message_id(in_reply_to)
@message = Message.find_by(source_id: in_reply_to)
@conversation = @message.conversation if @message.present?
@conversation_uuid = @conversation.uuid if @conversation.present?
end
# find conversation uuid from below pattern
# <conversation/#{@conversation.uuid}/messages/#{@messages&.last&.id}@#{@account.inbound_email_domain}>
def find_conversation_with_in_reply_to
match_result = nil
in_reply_to_addresses = mail.in_reply_to
in_reply_to_addresses = [in_reply_to_addresses] if in_reply_to_addresses.is_a?(String)
in_reply_to_addresses.each do |in_reply_to|
match_result = in_reply_to.match(::ApplicationMailbox::CONVERSATION_MESSAGE_ID_PATTERN)
break if match_result
end
find_by_in_reply_to_addresses(match_result, in_reply_to_addresses)
end
def find_by_in_reply_to_addresses(match_result, in_reply_to_addresses)
find_conversation_by_uuid(match_result) if match_result
find_conversation_by_message_id(in_reply_to_addresses) if @conversation.blank?
end
def validate_resource(resource)
Rails.logger.error "[App::Mailboxes::ReplyMailbox] Email conversation with uuid: #{conversation_uuid} not found" if resource.nil?
resource
@conversation.save!
Rails.logger.info "Created new conversation #{@conversation.id} for email #{mail.message_id}"
end
def decorate_mail
-94
View File
@@ -1,94 +0,0 @@
class SupportMailbox < ApplicationMailbox
include IncomingEmailValidityHelper
attr_accessor :channel, :account, :inbox, :conversation, :processed_mail
before_processing :find_channel,
:load_account,
:load_inbox,
:decorate_mail
def process
Rails.logger.info "Processing email #{mail.message_id} from #{original_sender_email} to #{mail.to} with subject #{mail.subject}"
# Skip processing email if it belongs to any of the edge cases
return unless incoming_email_from_valid_email?
ActiveRecord::Base.transaction do
find_or_create_contact
find_or_create_conversation
create_message
add_attachments_to_message
end
end
private
def find_channel
find_channel_with_to_mail if @channel.blank?
raise 'Email channel/inbox not found' if @channel.nil?
@channel
end
def find_channel_with_to_mail
@channel = EmailChannelFinder.new(mail).perform
end
def load_account
@account = @channel.account
end
def load_inbox
@inbox = @channel.inbox
end
def decorate_mail
@processed_mail = MailPresenter.new(mail, @account)
end
def find_conversation_by_in_reply_to
return if in_reply_to.blank?
@account.conversations.where("additional_attributes->>'in_reply_to' = ?", in_reply_to).first
end
def in_reply_to
mail['In-Reply-To'].try(:value)
end
def original_sender_email
@processed_mail.original_sender&.downcase
end
def find_or_create_conversation
@conversation = find_conversation_by_in_reply_to || ::Conversation.create!({
account_id: @account.id,
inbox_id: @inbox.id,
contact_id: @contact.id,
contact_inbox_id: @contact_inbox.id,
additional_attributes: {
in_reply_to: in_reply_to,
source: 'email',
auto_reply: @processed_mail.auto_reply?,
mail_subject: @processed_mail.subject,
initiated_at: {
timestamp: Time.now.utc
}
}
})
end
def find_or_create_contact
@contact = @inbox.contacts.from_email(original_sender_email)
if @contact.present?
@contact_inbox = ContactInbox.find_by(inbox: @inbox, contact: @contact)
else
create_contact
end
end
def identify_contact_name
processed_mail.sender_name || processed_mail.from.first.split('@').first
end
end
+17
View File
@@ -35,4 +35,21 @@ class ReportingEvent < ApplicationRecord
belongs_to :user, optional: true
belongs_to :inbox, optional: true
belongs_to :conversation, optional: true
# Scopes for filtering
scope :filter_by_date_range, lambda { |range|
where(created_at: range) if range.present?
}
scope :filter_by_inbox_id, lambda { |inbox_id|
where(inbox_id: inbox_id) if inbox_id.present?
}
scope :filter_by_user_id, lambda { |user_id|
where(user_id: user_id) if user_id.present?
}
scope :filter_by_name, lambda { |name|
where(name: name) if name.present?
}
end
+1
View File
@@ -3,6 +3,7 @@
# Table name: webhooks
#
# id :bigint not null, primary key
# name :string
# subscriptions :jsonb
# url :string
# webhook_type :integer default("account_type")
@@ -0,0 +1,29 @@
class Mailbox::ConversationFinder
DEFAULT_STRATEGIES = [
Mailbox::ConversationFinderStrategies::ReceiverUuidStrategy,
Mailbox::ConversationFinderStrategies::InReplyToStrategy,
Mailbox::ConversationFinderStrategies::ReferencesStrategy,
Mailbox::ConversationFinderStrategies::NewConversationStrategy
].freeze
def initialize(mail, strategies: DEFAULT_STRATEGIES)
@mail = mail
@strategies = strategies
end
def find
@strategies.each do |strategy_class|
conversation = strategy_class.new(@mail).find
next unless conversation
strategy_name = strategy_class.name.demodulize.underscore
Rails.logger.info "Conversation found via #{strategy_name} strategy"
return conversation
end
# Should not reach here if NewConversationStrategy is in the chain
Rails.logger.error 'No conversation found via any strategy (NewConversationStrategy missing?)'
nil
end
end
@@ -0,0 +1,13 @@
class Mailbox::ConversationFinderStrategies::BaseStrategy
attr_reader :mail
def initialize(mail)
@mail = mail
end
# Returns Conversation or nil
# Subclasses must implement this method
def find
raise NotImplementedError, "#{self.class} must implement #find"
end
end
@@ -0,0 +1,48 @@
class Mailbox::ConversationFinderStrategies::InReplyToStrategy < Mailbox::ConversationFinderStrategies::BaseStrategy
# Patterns from ApplicationMailbox
MESSAGE_PATTERN = %r{conversation/([a-zA-Z0-9-]+)/messages/(\d+)@}
# FALLBACK_PATTERN is used when building In-Reply-To headers in ConversationReplyMailer
# when there's no actual message to reply to (see app/mailers/conversation_reply_mailer.rb#in_reply_to_email).
# This happens when:
# - A conversation is started by an agent (no incoming message yet)
# - The conversation originated from a non-email channel (widget, WhatsApp, etc.) but is now using email
# - The incoming message doesn't have email metadata with a message_id
# In these cases, we use a conversation-level identifier instead of a message-level one.
FALLBACK_PATTERN = %r{account/(\d+)/conversation/([a-zA-Z0-9-]+)@}
def find
return nil if mail.in_reply_to.blank?
in_reply_to_addresses = Array.wrap(mail.in_reply_to)
in_reply_to_addresses.each do |in_reply_to|
# Try extracting UUID from patterns
uuid = extract_uuid_from_patterns(in_reply_to)
if uuid
conversation = Conversation.find_by(uuid: uuid)
return conversation if conversation
end
# Try finding by message source_id
message = Message.find_by(source_id: in_reply_to)
return message.conversation if message&.conversation
end
nil
end
private
def extract_uuid_from_patterns(message_id)
# Try message-specific pattern first
match = MESSAGE_PATTERN.match(message_id)
return match[1] if match
# Try conversation fallback pattern
match = FALLBACK_PATTERN.match(message_id)
return match[2] if match
nil
end
end
@@ -0,0 +1,83 @@
class Mailbox::ConversationFinderStrategies::NewConversationStrategy < Mailbox::ConversationFinderStrategies::BaseStrategy
include MailboxHelper
include IncomingEmailValidityHelper
attr_accessor :processed_mail, :account, :inbox, :contact, :contact_inbox, :conversation, :channel
def initialize(mail)
super(mail)
@channel = EmailChannelFinder.new(mail).perform
return unless @channel
@account = @channel.account
@inbox = @channel.inbox
@processed_mail = MailPresenter.new(mail, @account)
end
# This strategy prepares a new conversation but doesn't persist it yet.
# Why we don't use create! here:
# - Avoids orphan conversations if message/attachment creation fails later
# - Prevents duplicate conversations on job retry (no idempotency issue)
# - Follows the pattern from old SupportMailbox where everything was in one transaction
# The actual persistence happens in ReplyMailbox within a transaction that includes message creation.
def find
return nil unless @channel # No valid channel found
return nil unless incoming_email_from_valid_email? # Skip edge cases
# Check if conversation already exists by in_reply_to
existing_conversation = find_conversation_by_in_reply_to
return existing_conversation if existing_conversation
# Prepare contact (persisted) and build conversation (not persisted)
find_or_create_contact
build_conversation
end
private
def find_or_create_contact
@contact = @inbox.contacts.from_email(original_sender_email)
if @contact.present?
@contact_inbox = ContactInbox.find_by(inbox: @inbox, contact: @contact)
else
create_contact
end
end
def original_sender_email
@processed_mail.original_sender&.downcase
end
def identify_contact_name
@processed_mail.sender_name || @processed_mail.from.first.split('@').first
end
def build_conversation
# Build but don't persist - ReplyMailbox will save in transaction with message
@conversation = ::Conversation.new(
account_id: @account.id,
inbox_id: @inbox.id,
contact_id: @contact.id,
contact_inbox_id: @contact_inbox.id,
additional_attributes: {
in_reply_to: in_reply_to,
source: 'email',
auto_reply: @processed_mail.auto_reply?,
mail_subject: @processed_mail.subject,
initiated_at: {
timestamp: Time.now.utc
}
}
)
end
def in_reply_to
mail['In-Reply-To'].try(:value)
end
def find_conversation_by_in_reply_to
return if in_reply_to.blank?
@account.conversations.where("additional_attributes->>'in_reply_to' = ?", in_reply_to).first
end
end
@@ -0,0 +1,26 @@
class Mailbox::ConversationFinderStrategies::ReceiverUuidStrategy < Mailbox::ConversationFinderStrategies::BaseStrategy
# Pattern from ApplicationMailbox::REPLY_EMAIL_UUID_PATTERN
UUID_PATTERN = /^reply\+([0-9a-f]{8}\b-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-\b[0-9a-f]{12})$/i
def find
uuid = extract_uuid_from_receivers
return nil unless uuid
Conversation.find_by(uuid: uuid)
end
private
def extract_uuid_from_receivers
mail_presenter = MailPresenter.new(mail)
return nil if mail_presenter.mail_receiver.blank?
mail_presenter.mail_receiver.each do |email|
username = email.split('@').first
match = username.match(UUID_PATTERN)
return match[1] if match
end
nil
end
end
@@ -0,0 +1,59 @@
class Mailbox::ConversationFinderStrategies::ReferencesStrategy < Mailbox::ConversationFinderStrategies::BaseStrategy
# Patterns from ApplicationMailbox
MESSAGE_PATTERN = %r{conversation/([a-zA-Z0-9-]+)/messages/(\d+)@}
# FALLBACK_PATTERN is used when building References headers in ConversationReplyMailer
# when there's no actual message to reply to (see app/mailers/conversation_reply_mailer.rb#in_reply_to_email).
# This happens when:
# - A conversation is started by an agent (no incoming message yet)
# - The conversation originated from a non-email channel (widget, WhatsApp, etc.) but is now using email
# - The incoming message doesn't have email metadata with a message_id
# In these cases, we use a conversation-level identifier instead of a message-level one.
FALLBACK_PATTERN = %r{account/(\d+)/conversation/([a-zA-Z0-9-]+)@}
def initialize(mail)
super(mail)
# Get channel once upfront to use for scoped queries
@channel = EmailChannelFinder.new(mail).perform
end
def find
return nil if mail.references.blank?
return nil unless @channel # No valid channel found
references = Array.wrap(mail.references)
references.each do |reference|
conversation = find_conversation_from_reference(reference)
return conversation if conversation
end
nil
end
private
def find_conversation_from_reference(reference)
# Try extracting UUID from patterns
uuid = extract_uuid_from_patterns(reference)
if uuid
# Query scoped to inbox - prevents cross-account/cross-inbox matches at database level
conversation = Conversation.find_by(uuid: uuid, inbox_id: @channel.inbox.id)
return conversation if conversation
end
# We scope to the inbox, that way we filter out messages and conversations that don't belong to the channel
message = Message.find_by(source_id: reference, inbox_id: @channel.inbox.id)
message&.conversation
end
def extract_uuid_from_patterns(message_id)
match = MESSAGE_PATTERN.match(message_id)
return match[1] if match
match = FALLBACK_PATTERN.match(message_id)
return match[2] if match
nil
end
end
@@ -1,4 +1,5 @@
json.id webhook.id
json.name webhook.name
json.url webhook.url
json.account_id webhook.account_id
json.subscriptions webhook.subscriptions
+12 -12
View File
@@ -186,8 +186,8 @@ tr:
error_code: 'Hata kodu: %{error_code}'
activity:
captain:
resolved: 'Sohbet, %{user_name} tarafından etkinlik olmadığı için çözüldü olarak işaretlendi'
open: 'Sohbet, %{user_name} tarafından açık olarak işaretlendi'
resolved: 'Konuşma, %{user_name} tarafından etkinlik olmadığı için çözüldü olarak işaretlendi'
open: 'Konuşma, %{user_name} tarafından açık olarak işaretlendi'
agent_bot:
error_moved_to_open: 'Conversation was marked open by system due to an error with the agent bot.'
status:
@@ -196,9 +196,9 @@ tr:
open: 'Konuşma %{user_name} tarafından açık olarak işaretlendi'
pending: 'Konuşma, %{user_name} tarafından bekleyen olarak işaretlendi'
snoozed: 'Konuşma, %{user_name} tarafından erteledi olarak işaretlendi'
auto_resolved_days: ' %{count} günlük hareketsizlik nedeniyle konuşma, sistem tarafından çözümlendi olarak işaretlendi'
auto_resolved_hours: 'Sohbet, sistem tarafından %{count} saat etkinlik olmadığı için çözüldü olarak işaretlendi'
auto_resolved_minutes: 'Sohbet, sistem tarafından %{count} dakika etkinlik olmadığı için çözüldü olarak işaretlendi'
auto_resolved_days: 'Konuşma, %{count} günlük hareketsizlik nedeniyle sistem tarafından çözümlendi olarak işaretlendi'
auto_resolved_hours: 'Konuşma, %{count} saatlik hareketsizlik nedeniyle sistem tarafından çözümlendi olarak işaretlendi'
auto_resolved_minutes: 'Konuşma, %{count} dakikalık hareketsizlik nedeniyle sistem tarafından çözümlendi olarak işaretlendi'
system_auto_open: Sistem, yeni gelen bir mesaj nedeniyle konuşmayı tekrar açtı.
priority:
added: '%{user_name} önceliği %{new_priority} olarak ayarladı'
@@ -217,7 +217,7 @@ tr:
removed: '%{user_name}, %{labels} kaldırdı'
sla:
added: '%{user_name} added SLA policy %{sla_name}'
removed: '%{user_name} removed SLA policy %{sla_name}'
removed: '%{user_name}, %{sla_name} adlı SLA politikasını kaldırdı'
linear:
issue_created: 'Linear sorun %{issue_id} %{user_name} tarafından oluşturuldu'
issue_linked: 'Linear sorun %{issue_id} %{user_name} tarafından bağlandı'
@@ -265,7 +265,7 @@ tr:
meeting_name: '%{agent_name} bir toplantı başlattı'
slack:
name: 'Slack'
short_description: 'Slack üzerinden doğrudan bildirim alın ve sohbetlere yanıt verin.'
short_description: 'Doğrudan Slack''te bildirimler alın ve konuşmalara yanıt verin.'
description: "Integrate Chatwoot with Slack to keep your team in sync. This integration allows you to receive notifications for new conversations and respond to them directly within Slack's interface."
webhooks:
name: 'Webhooks'
@@ -390,21 +390,21 @@ tr:
automation:
system_name: 'Otomasyon Sistemi'
crm:
no_message: 'Sohbette mesaj yok'
no_message: 'Konuşmada mesaj yok'
attachment: '[Ek: %{type}]'
no_content: '[No content]'
no_content: '[İçerik yok]'
created_activity: |
Yeni sohbet başlatıldı: %{brand_name}
Yeni konuşma başlatıldı: %{brand_name}
Kanal: %{channel_info}
Oluşturulma: %{formatted_creation_time}
Sohbet Kimliği: %{display_id}
Konuşma Kimliği: %{display_id}
%{brand_name}'de görüntüle: %{url}
transcript_activity: |
%{brand_name} sohbet dökümü
Kanal: %{channel_info}
Sohbet Kimliği: %{display_id}
Konuşma Kimliği: %{display_id}
%{brand_name}'de görüntüle: %{url}
Döküm:

Some files were not shown because too many files have changed in this diff Show More