Merge branch 'develop' into assignment_v2/assignment_service

This commit is contained in:
Tanmay Deep Sharma
2025-10-05 15:02:08 +02:00
committed by GitHub
343 changed files with 4521 additions and 777 deletions

View File

@@ -0,0 +1,54 @@
class Email::BaseBuilder
pattr_initialize [:inbox!]
private
def channel
@channel ||= inbox.channel
end
def account
@account ||= inbox.account
end
def conversation
@conversation ||= message.conversation
end
def custom_sender_name
message&.sender&.available_name || I18n.t('conversations.reply.email.header.notifications')
end
def sender_name(sender_email)
# Friendly: <agent_name> from <business_name>
# Professional: <business_name>
if inbox.friendly?
I18n.t(
'conversations.reply.email.header.friendly_name',
sender_name: custom_sender_name,
business_name: business_name,
from_email: sender_email
)
else
I18n.t(
'conversations.reply.email.header.professional_name',
business_name: business_name,
from_email: sender_email
)
end
end
def business_name
inbox.business_name || inbox.sanitized_name
end
def account_support_email
# Parse the email to ensure it's in the correct format, the user
# can save it in the format "Name <email@domain.com>"
parse_email(account.support_email)
end
def parse_email(email_string)
Mail::Address.new(email_string).address
end
end

View File

@@ -0,0 +1,51 @@
class Email::FromBuilder < Email::BaseBuilder
pattr_initialize [:inbox!, :message!]
def build
return sender_name(account_support_email) unless inbox.email?
from_email = case email_channel_type
when :standard_imap_smtp,
:google_oauth,
:microsoft_oauth,
:forwarding_own_smtp
channel.email
when :imap_chatwoot_smtp,
:forwarding_chatwoot_smtp
channel.verified_for_sending ? channel.email : account_support_email
else
account_support_email
end
sender_name(from_email)
end
private
def email_channel_type
return :google_oauth if channel.google?
return :microsoft_oauth if channel.microsoft?
return :standard_imap_smtp if imap_and_smtp_enabled?
return :imap_chatwoot_smtp if imap_enabled_without_smtp?
return :forwarding_own_smtp if forwarding_with_own_smtp?
return :forwarding_chatwoot_smtp if forwarding_without_smtp?
:unknown
end
def imap_and_smtp_enabled?
channel.imap_enabled && channel.smtp_enabled
end
def imap_enabled_without_smtp?
channel.imap_enabled && !channel.smtp_enabled
end
def forwarding_with_own_smtp?
!channel.imap_enabled && channel.smtp_enabled
end
def forwarding_without_smtp?
!channel.imap_enabled && !channel.smtp_enabled
end
end

View File

@@ -0,0 +1,21 @@
class Email::ReplyToBuilder < Email::BaseBuilder
pattr_initialize [:inbox!, :message!]
def build
reply_to = if inbox.email?
channel.email
elsif inbound_email_enabled?
"reply+#{conversation.uuid}@#{account.inbound_email_domain}"
else
account_support_email
end
sender_name(reply_to)
end
private
def inbound_email_enabled?
account.feature_enabled?('inbound_emails') && account.inbound_email_domain.present?
end
end

View File

@@ -7,6 +7,7 @@ class Messages::MessageBuilder
@private = params[:private] || false
@conversation = conversation
@user = user
@account = conversation.account
@message_type = params[:message_type] || 'outgoing'
@attachments = params[:attachments]
@automation_rule = content_attributes&.dig(:automation_rule_id)
@@ -20,6 +21,9 @@ class Messages::MessageBuilder
@message = @conversation.messages.build(message_params)
process_attachments
process_emails
# When the message has no quoted content, it will just be rendered as a regular message
# The frontend is equipped to handle this case
process_email_content if @account.feature_enabled?(:quoted_email_reply)
@message.save!
@message
end
@@ -92,6 +96,14 @@ class Messages::MessageBuilder
@message.content_attributes[:to_emails] = to_emails
end
def process_email_content
return unless should_process_email_content?
@message.content_attributes ||= {}
email_attributes = build_email_attributes
@message.content_attributes[:email] = email_attributes
end
def process_email_string(email_string)
return [] if email_string.blank?
@@ -153,4 +165,52 @@ class Messages::MessageBuilder
source_id: @params[:source_id]
}.merge(external_created_at).merge(automation_rule_id).merge(campaign_id).merge(template_params)
end
def email_inbox?
@conversation.inbox&.inbox_type == 'Email'
end
def should_process_email_content?
email_inbox? && !@private && @message.content.present?
end
def build_email_attributes
email_attributes = ensure_indifferent_access(@message.content_attributes[:email] || {})
normalized_content = normalize_email_body(@message.content)
email_attributes[:html_content] = build_html_content(normalized_content)
email_attributes[:text_content] = build_text_content(normalized_content)
email_attributes
end
def build_html_content(normalized_content)
html_content = ensure_indifferent_access(@message.content_attributes.dig(:email, :html_content) || {})
rendered_html = render_email_html(normalized_content)
html_content[:full] = rendered_html
html_content[:reply] = rendered_html
html_content
end
def build_text_content(normalized_content)
text_content = ensure_indifferent_access(@message.content_attributes.dig(:email, :text_content) || {})
text_content[:full] = normalized_content
text_content[:reply] = normalized_content
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
end

View File

@@ -17,8 +17,8 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController
before_action :set_include_contact_inboxes, only: [:index, :active, :search, :filter, :show, :update]
def index
@contacts_count = resolved_contacts.count
@contacts = fetch_contacts(resolved_contacts)
@contacts_count = @contacts.total_count
end
def search
@@ -29,8 +29,8 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController
OR contacts.additional_attributes->>\'company_name\' ILIKE :search',
search: "%#{params[:q].strip}%"
)
@contacts_count = contacts.count
@contacts = fetch_contacts(contacts)
@contacts_count = @contacts.total_count
end
def import
@@ -55,8 +55,8 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController
def active
contacts = Current.account.contacts.where(id: ::OnlineStatusTracker
.get_available_contact_ids(Current.account.id))
@contacts_count = contacts.count
@contacts = fetch_contacts(contacts)
@contacts_count = @contacts.total_count
end
def show; end
@@ -133,13 +133,14 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController
end
def fetch_contacts(contacts)
contacts_with_avatar = filtrate(contacts)
.includes([{ avatar_attachment: [:blob] }])
.page(@current_page).per(RESULTS_PER_PAGE)
# Build includes hash to avoid separate query when contact_inboxes are needed
includes_hash = { avatar_attachment: [:blob] }
includes_hash[:contact_inboxes] = { inbox: :channel } if @include_contact_inboxes
return contacts_with_avatar.includes([{ contact_inboxes: [:inbox] }]) if @include_contact_inboxes
contacts_with_avatar
filtrate(contacts)
.includes(includes_hash)
.page(@current_page)
.per(RESULTS_PER_PAGE)
end
def build_contact_inbox

View File

@@ -4,7 +4,8 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController
before_action :fetch_agent_bot, only: [:set_agent_bot]
before_action :validate_limit, only: [:create]
# we are already handling the authorization in fetch inbox
before_action :check_authorization, except: [:show]
before_action :check_authorization, except: [:show, :health]
before_action :validate_whatsapp_cloud_channel, only: [:health]
def index
@inboxes = policy_scope(Current.account.inboxes.order_by_name.includes(:channel, { avatar_attachment: [:blob] }))
@@ -78,6 +79,14 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController
render status: :internal_server_error, json: { error: e.message }
end
def health
health_data = Whatsapp::HealthService.new(@inbox.channel).fetch_health_status
render json: health_data
rescue StandardError => e
Rails.logger.error "[INBOX HEALTH] Error fetching health data: #{e.message}"
render json: { error: e.message }, status: :unprocessable_entity
end
private
def fetch_inbox
@@ -89,6 +98,12 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController
@agent_bot = AgentBot.find(params[:agent_bot]) if params[:agent_bot]
end
def validate_whatsapp_cloud_channel
return if @inbox.channel.is_a?(Channel::Whatsapp) && @inbox.channel.provider == 'whatsapp_cloud'
render json: { error: 'Health data only available for WhatsApp Cloud API channels' }, status: :bad_request
end
def create_channel
return unless allowed_channel_types.include?(permitted_params[:channel][:type])

View File

@@ -0,0 +1,14 @@
/* global axios */
import ApiClient from './ApiClient';
class InboxHealthAPI extends ApiClient {
constructor() {
super('inboxes', { accountScoped: true });
}
getHealthStatus(inboxId) {
return axios.get(`${this.url}/${inboxId}/health`);
}
}
export default new InboxHealthAPI();

View File

@@ -148,10 +148,21 @@ const isAnyDropdownActive = computed(() => {
const handleContactSearch = value => {
showContactsDropdown.value = true;
emit('searchContacts', {
keys: ['email', 'phone_number', 'name'],
query: value,
const query = typeof value === 'string' ? value.trim() : '';
const hasAlphabet = Array.from(query).some(char => {
const lower = char.toLowerCase();
const upper = char.toUpperCase();
return lower !== upper;
});
const isEmailLike = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(query);
const keys = ['email', 'phone_number', 'name'].filter(key => {
if (key === 'phone_number' && hasAlphabet) return false;
if (key === 'name' && isEmailLike) return false;
return true;
});
emit('searchContacts', { keys, query: value });
};
const handleDropdownUpdate = (type, value) => {

View File

@@ -45,7 +45,7 @@ const activeAssistantLabel = computed(() => {
/>
</template>
<DropdownBody class="bottom-9 min-w-64 z-50" strong>
<DropdownSection class="max-h-80 overflow-scroll">
<DropdownSection class="[&>ul]:max-h-80">
<DropdownItem
v-for="assistant in assistants"
:key="assistant.id"

View File

@@ -91,7 +91,7 @@ const updateSelected = newValue => {
:class="dropdownPosition"
strong
>
<DropdownSection class="max-h-80 overflow-scroll">
<DropdownSection class="[&>ul]:max-h-80">
<DropdownItem
v-for="option in options"
:key="option.value"

View File

@@ -123,7 +123,7 @@ const toggleOption = option => {
</Button>
</template>
<DropdownBody class="top-0 min-w-48 z-50" strong>
<DropdownSection class="max-h-80 overflow-scroll">
<DropdownSection class="[&>ul]:max-h-80">
<DropdownItem
v-for="option in options"
:key="option.id"

View File

@@ -124,7 +124,7 @@ const toggleSelected = option => {
:placeholder="searchPlaceholder || t('COMBOBOX.SEARCH_PLACEHOLDER')"
/>
</div>
<DropdownSection class="max-h-80 overflow-scroll">
<DropdownSection class="[&>ul]:max-h-80">
<template v-if="searchResults.length">
<DropdownItem
v-for="option in searchResults"

View File

@@ -5,9 +5,9 @@ import { sanitizeTextForRender } from '@chatwoot/utils';
import { allowedCssProperties } from 'lettersanitizer';
import Icon from 'next/icon/Icon.vue';
import { EmailQuoteExtractor } from './removeReply.js';
import BaseBubble from 'next/message/bubbles/Base.vue';
import { EmailQuoteExtractor } from 'dashboard/helper/emailQuoteExtractor.js';
import FormattedContent from 'next/message/bubbles/Text/FormattedContent.vue';
import BaseBubble from 'next/message/bubbles/Base.vue';
import AttachmentChips from 'next/message/chips/AttachmentChips.vue';
import EmailMeta from './EmailMeta.vue';
import TranslationToggle from 'dashboard/components-next/message/TranslationToggle.vue';
@@ -47,6 +47,13 @@ const originalEmailHtml = computed(
originalEmailText.value
);
const hasEmailContent = computed(() => {
return (
contentAttributes?.value?.email?.textContent?.full ||
contentAttributes?.value?.email?.htmlContent?.full
);
});
const messageContent = computed(() => {
// If translations exist and we're showing translations (not original)
if (hasTranslations.value && !renderOriginal.value) {
@@ -137,7 +144,7 @@ const handleSeeOriginal = () => {
</button>
</div>
<FormattedContent
v-if="isOutgoing && content"
v-if="isOutgoing && content && !hasEmailContent"
class="text-n-slate-12"
:content="messageContent"
/>

View File

@@ -76,7 +76,7 @@ function changeAvailabilityStatus(availability) {
</script>
<template>
<DropdownSection>
<DropdownSection class="[&>ul]:overflow-visible">
<div class="grid gap-0">
<DropdownItem preserve-open>
<div class="flex-grow flex items-center gap-1">

View File

@@ -118,6 +118,14 @@ export default {
type: String,
default: '',
},
showQuotedReplyToggle: {
type: Boolean,
default: false,
},
quotedReplyEnabled: {
type: Boolean,
default: false,
},
},
emits: [
'replaceText',
@@ -125,6 +133,7 @@ export default {
'toggleEditor',
'selectWhatsappTemplate',
'selectContentTemplate',
'toggleQuotedReply',
],
setup() {
const { setSignatureFlagForInbox, fetchSignatureFlagFromUISettings } =
@@ -249,6 +258,11 @@ export default {
isFetchingAppIntegrations() {
return this.uiFlags.isFetching;
},
quotedReplyToggleTooltip() {
return this.quotedReplyEnabled
? this.$t('CONVERSATION.REPLYBOX.QUOTED_REPLY.DISABLE_TOOLTIP')
: this.$t('CONVERSATION.REPLYBOX.QUOTED_REPLY.ENABLE_TOOLTIP');
},
},
mounted() {
ActiveStorage.start();
@@ -339,6 +353,16 @@ export default {
sm
@click="toggleMessageSignature"
/>
<NextButton
v-if="showQuotedReplyToggle"
v-tooltip.top-end="quotedReplyToggleTooltip"
icon="i-ph-quotes"
:variant="quotedReplyEnabled ? 'solid' : 'faded'"
color="slate"
sm
:aria-pressed="quotedReplyEnabled"
@click="$emit('toggleQuotedReply')"
/>
<NextButton
v-if="enableWhatsAppTemplates"
v-tooltip.top-end="$t('CONVERSATION.FOOTER.WHATSAPP_TEMPLATES')"

View File

@@ -0,0 +1,76 @@
<script setup>
import { computed, ref } from 'vue';
import { useMessageFormatter } from 'shared/composables/useMessageFormatter';
import { useI18n } from 'vue-i18n';
import NextButton from 'dashboard/components-next/button/Button.vue';
const props = defineProps({
quotedEmailText: {
type: String,
required: true,
},
previewText: {
type: String,
required: true,
},
});
const emit = defineEmits(['toggle']);
const { t } = useI18n();
const { formatMessage } = useMessageFormatter();
const isExpanded = ref(false);
const formattedQuotedEmailText = computed(() => {
if (!props.quotedEmailText) {
return '';
}
return formatMessage(props.quotedEmailText, false, false, true);
});
const toggleExpand = () => {
isExpanded.value = !isExpanded.value;
};
</script>
<template>
<div class="mt-2">
<div
class="relative rounded-md px-3 py-2 text-xs text-n-slate-12 bg-n-slate-3 dark:bg-n-solid-3"
>
<div class="absolute top-2 right-2 z-10 flex items-center gap-1">
<NextButton
v-tooltip="
isExpanded
? t('CONVERSATION.REPLYBOX.QUOTED_REPLY.COLLAPSE')
: t('CONVERSATION.REPLYBOX.QUOTED_REPLY.EXPAND')
"
ghost
slate
xs
:icon="isExpanded ? 'i-lucide-minimize' : 'i-lucide-maximize'"
@click="toggleExpand"
/>
<NextButton
v-tooltip="t('CONVERSATION.REPLYBOX.QUOTED_REPLY.REMOVE_PREVIEW')"
ghost
slate
xs
icon="i-lucide-x"
@click="emit('toggle')"
/>
</div>
<div
v-dompurify-html="formattedQuotedEmailText"
class="w-full max-w-none break-words prose prose-sm dark:prose-invert cursor-pointer ltr:pr-8 rtl:pl-8"
:class="{
'line-clamp-1': !isExpanded,
'max-h-60 overflow-y-auto': isExpanded,
}"
:title="previewText"
@click="toggleExpand"
/>
</div>
</div>
</template>

View File

@@ -5,6 +5,7 @@ import { useAlert } from 'dashboard/composables';
import { useUISettings } from 'dashboard/composables/useUISettings';
import { useTrack } from 'dashboard/composables';
import keyboardEventListenerMixins from 'shared/mixins/keyboardEventListenerMixins';
import { FEATURE_FLAGS } from 'dashboard/featureFlags';
import CannedResponse from './CannedResponse.vue';
import ReplyToMessage from './ReplyToMessage.vue';
@@ -16,6 +17,7 @@ import ReplyBottomPanel from 'dashboard/components/widgets/WootWriter/ReplyBotto
import ArticleSearchPopover from 'dashboard/routes/dashboard/helpcenter/components/ArticleSearch/SearchPopover.vue';
import MessageSignatureMissingAlert from './MessageSignatureMissingAlert.vue';
import ReplyBoxBanner from './ReplyBoxBanner.vue';
import QuotedEmailPreview from './QuotedEmailPreview.vue';
import { REPLY_EDITOR_MODES } from 'dashboard/components/widgets/WootWriter/constants';
import WootMessageEditor from 'dashboard/components/widgets/WootWriter/Editor.vue';
import AudioRecorder from 'dashboard/components/widgets/WootWriter/AudioRecorder.vue';
@@ -32,6 +34,12 @@ import { MESSAGE_MAX_LENGTH } from 'shared/helpers/MessageTypeHelper';
import inboxMixin, { INBOX_FEATURES } from 'shared/mixins/inboxMixin';
import { trimContent, debounce, getRecipients } from '@chatwoot/utils';
import wootConstants from 'dashboard/constants/globals';
import {
extractQuotedEmailText,
buildQuotedEmailHeader,
truncatePreviewText,
appendQuotedTextToMessage,
} from 'dashboard/helper/quotedEmailHelper';
import { CONVERSATION_EVENTS } from '../../../helper/AnalyticsHelper/events';
import fileUploadMixin from 'dashboard/mixins/fileUploadMixin';
import {
@@ -65,6 +73,7 @@ export default {
ContentTemplates,
WhatsappTemplates,
WootMessageEditor,
QuotedEmailPreview,
},
mixins: [inboxMixin, fileUploadMixin, keyboardEventListenerMixins],
props: {
@@ -80,6 +89,8 @@ export default {
updateUISettings,
isEditorHotKeyEnabled,
fetchSignatureFlagFromUISettings,
setQuotedReplyFlagForInbox,
fetchQuotedReplyFlagFromUISettings,
} = useUISettings();
const replyEditor = useTemplateRef('replyEditor');
@@ -89,6 +100,8 @@ export default {
updateUISettings,
isEditorHotKeyEnabled,
fetchSignatureFlagFromUISettings,
setQuotedReplyFlagForInbox,
fetchQuotedReplyFlagFromUISettings,
replyEditor,
};
},
@@ -130,6 +143,8 @@ export default {
currentUser: 'getCurrentUser',
lastEmail: 'getLastEmailInSelectedChat',
globalConfig: 'globalConfig/get',
accountId: 'getCurrentAccountId',
isFeatureEnabledonAccount: 'accounts/isFeatureEnabledonAccount',
}),
currentContact() {
return this.$store.getters['contacts/getContact'](
@@ -367,6 +382,51 @@ export default {
const { slug = '' } = portal;
return slug;
},
isQuotedEmailReplyEnabled() {
return this.isFeatureEnabledonAccount(
this.accountId,
FEATURE_FLAGS.QUOTED_EMAIL_REPLY
);
},
quotedReplyPreference() {
if (!this.isAnEmailChannel || !this.isQuotedEmailReplyEnabled) {
return false;
}
return !!this.fetchQuotedReplyFlagFromUISettings(this.channelType);
},
lastEmailWithQuotedContent() {
if (!this.isAnEmailChannel) {
return null;
}
const lastEmail = this.lastEmail;
if (!lastEmail || lastEmail.private) {
return null;
}
return lastEmail;
},
quotedEmailText() {
return extractQuotedEmailText(this.lastEmailWithQuotedContent);
},
quotedEmailPreviewText() {
return truncatePreviewText(this.quotedEmailText, 80);
},
shouldShowQuotedReplyToggle() {
return (
this.isAnEmailChannel &&
!this.isOnPrivateNote &&
this.isQuotedEmailReplyEnabled
);
},
shouldShowQuotedPreview() {
return (
this.shouldShowQuotedReplyToggle &&
this.quotedReplyPreference &&
!!this.quotedEmailText
);
},
},
watch: {
currentChat(conversation, oldConversation) {
@@ -516,6 +576,36 @@ export default {
);
}
},
toggleQuotedReply() {
if (!this.isAnEmailChannel) {
return;
}
const nextValue = !this.quotedReplyPreference;
this.setQuotedReplyFlagForInbox(this.channelType, nextValue);
},
shouldIncludeQuotedEmail() {
return (
this.isQuotedEmailReplyEnabled &&
this.quotedReplyPreference &&
this.shouldShowQuotedReplyToggle &&
!!this.quotedEmailText
);
},
getMessageWithQuotedEmailText(message) {
if (!this.shouldIncludeQuotedEmail()) {
return message;
}
const quotedText = this.quotedEmailText || '';
const header = buildQuotedEmailHeader(
this.lastEmailWithQuotedContent,
this.currentContact,
this.inbox
);
return appendQuotedTextToMessage(message, quotedText, header);
},
resetRecorderAndClearAttachments() {
// Reset audio recorder UI state
this.resetAudioRecorderInput();
@@ -965,9 +1055,11 @@ export default {
return multipleMessagePayload;
},
getMessagePayload(message) {
const messageWithQuote = this.getMessageWithQuotedEmailText(message);
let messagePayload = {
conversationId: this.currentChat.id,
message,
message: messageWithQuote,
private: this.isPrivate,
sender: this.sender,
};
@@ -995,7 +1087,6 @@ export default {
if (this.toEmails && !this.isOnPrivateNote) {
messagePayload.toEmails = this.toEmails;
}
return messagePayload;
},
setCcEmails(value) {
@@ -1160,6 +1251,12 @@ export default {
@toggle-variables-menu="toggleVariablesMenu"
@clear-selection="clearEditorSelection"
/>
<QuotedEmailPreview
v-if="shouldShowQuotedPreview"
:quoted-email-text="quotedEmailText"
:preview-text="quotedEmailPreviewText"
@toggle="toggleQuotedReply"
/>
</div>
<div
v-if="hasAttachments && !showAudioRecorderEditor"
@@ -1195,6 +1292,8 @@ export default {
:show-editor-toggle="isAPIInbox && !isOnPrivateNote"
:show-emoji-picker="showEmojiPicker"
:show-file-upload="showFileUpload"
:show-quoted-reply-toggle="shouldShowQuotedReplyToggle"
:quoted-reply-enabled="quotedReplyPreference"
:toggle-audio-recorder-play-pause="toggleAudioRecorderPlayPause"
:toggle-audio-recorder="toggleAudioRecorder"
:toggle-emoji-picker="toggleEmojiPicker"
@@ -1206,6 +1305,7 @@ export default {
@toggle-editor="toggleRichContentEditor"
@replace-text="replaceText"
@toggle-insert-article="toggleInsertArticle"
@toggle-quoted-reply="toggleQuotedReply"
/>
<WhatsappTemplates
:inbox-id="inbox.id"

View File

@@ -13,6 +13,7 @@ const getUISettingsMock = ref({
conversation_sidebar_items_order: DEFAULT_CONVERSATION_SIDEBAR_ITEMS_ORDER,
contact_sidebar_items_order: DEFAULT_CONTACT_SIDEBAR_ITEMS_ORDER,
editor_message_key: 'enter',
channel_email_quoted_reply_enabled: true,
});
vi.mock('dashboard/composables/store', () => ({
@@ -37,6 +38,7 @@ describe('useUISettings', () => {
DEFAULT_CONVERSATION_SIDEBAR_ITEMS_ORDER,
contact_sidebar_items_order: DEFAULT_CONTACT_SIDEBAR_ITEMS_ORDER,
editor_message_key: 'enter',
channel_email_quoted_reply_enabled: true,
});
});
@@ -51,6 +53,7 @@ describe('useUISettings', () => {
DEFAULT_CONVERSATION_SIDEBAR_ITEMS_ORDER,
contact_sidebar_items_order: DEFAULT_CONTACT_SIDEBAR_ITEMS_ORDER,
editor_message_key: 'enter',
channel_email_quoted_reply_enabled: true,
},
});
});
@@ -65,6 +68,7 @@ describe('useUISettings', () => {
DEFAULT_CONVERSATION_SIDEBAR_ITEMS_ORDER,
contact_sidebar_items_order: DEFAULT_CONTACT_SIDEBAR_ITEMS_ORDER,
editor_message_key: 'enter',
channel_email_quoted_reply_enabled: true,
},
});
});
@@ -100,6 +104,7 @@ describe('useUISettings', () => {
contact_sidebar_items_order: DEFAULT_CONTACT_SIDEBAR_ITEMS_ORDER,
email_signature_enabled: true,
editor_message_key: 'enter',
channel_email_quoted_reply_enabled: true,
},
});
});
@@ -109,6 +114,26 @@ describe('useUISettings', () => {
expect(fetchSignatureFlagFromUISettings('email')).toBe(undefined);
});
it('sets quoted reply flag for inbox correctly', () => {
const { setQuotedReplyFlagForInbox } = useUISettings();
setQuotedReplyFlagForInbox('Channel::Email', false);
expect(mockDispatch).toHaveBeenCalledWith('updateUISettings', {
uiSettings: {
is_ct_labels_open: true,
conversation_sidebar_items_order:
DEFAULT_CONVERSATION_SIDEBAR_ITEMS_ORDER,
contact_sidebar_items_order: DEFAULT_CONTACT_SIDEBAR_ITEMS_ORDER,
editor_message_key: 'enter',
channel_email_quoted_reply_enabled: false,
},
});
});
it('fetches quoted reply flag from UI settings correctly', () => {
const { fetchQuotedReplyFlagFromUISettings } = useUISettings();
expect(fetchQuotedReplyFlagFromUISettings('Channel::Email')).toBe(true);
});
it('returns correct value for isEditorHotKeyEnabled when editor_message_key is configured', () => {
getUISettingsMock.value.enter_to_send_enabled = false;
const { isEditorHotKeyEnabled } = useUISettings();

View File

@@ -87,6 +87,13 @@ const setSignatureFlagForInbox = (channelType, value, updateUISettings) => {
updateUISettings({ [`${slugifiedChannel}_signature_enabled`]: value });
};
const setQuotedReplyFlagForInbox = (channelType, value, updateUISettings) => {
if (!channelType) return;
const slugifiedChannel = slugifyChannel(channelType);
updateUISettings({ [`${slugifiedChannel}_quoted_reply_enabled`]: value });
};
/**
* Fetches the signature flag for a specific channel type from UI settings.
* @param {string} channelType - The type of the channel.
@@ -100,6 +107,13 @@ const fetchSignatureFlagFromUISettings = (channelType, uiSettings) => {
return uiSettings.value[`${slugifiedChannel}_signature_enabled`];
};
const fetchQuotedReplyFlagFromUISettings = (channelType, uiSettings) => {
if (!channelType) return false;
const slugifiedChannel = slugifyChannel(channelType);
return uiSettings.value[`${slugifiedChannel}_quoted_reply_enabled`];
};
/**
* Checks if a specific editor hotkey is enabled.
* @param {string} key - The key to check.
@@ -147,6 +161,10 @@ export function useUISettings() {
setSignatureFlagForInbox(channelType, value, updateUISettings),
fetchSignatureFlagFromUISettings: channelType =>
fetchSignatureFlagFromUISettings(channelType, uiSettings),
setQuotedReplyFlagForInbox: (channelType, value) =>
setQuotedReplyFlagForInbox(channelType, value, updateUISettings),
fetchQuotedReplyFlagFromUISettings: channelType =>
fetchQuotedReplyFlagFromUISettings(channelType, uiSettings),
isEditorHotKeyEnabled: key => isEditorHotKeyEnabled(key, uiSettings),
};
}

View File

@@ -40,6 +40,7 @@ export const FEATURE_FLAGS = {
CONTACT_CHATWOOT_SUPPORT_TEAM: 'contact_chatwoot_support_team',
CAPTAIN_V2: 'captain_integration_v2',
SAML: 'saml',
QUOTED_EMAIL_REPLY: 'quoted_email_reply',
};
export const PREMIUM_FEATURES = [

View File

@@ -10,6 +10,8 @@ const QUOTE_INDICATORS = [
'[class*="Quote"]',
];
const BLOCKQUOTE_FALLBACK_SELECTOR = 'blockquote';
// Regex patterns for quote identification
const QUOTE_PATTERNS = [
/On .* wrote:/i,
@@ -36,6 +38,8 @@ export class EmailQuoteExtractor {
});
});
this.removeTrailingBlockquote(tempDiv);
// Remove text-based quotes
const textNodeQuotes = this.findTextNodeQuotes(tempDiv);
textNodeQuotes.forEach(el => {
@@ -62,6 +66,10 @@ export class EmailQuoteExtractor {
}
}
if (this.findTrailingBlockquote(tempDiv)) {
return true;
}
// Check for text-based quotes
const textNodeQuotes = this.findTextNodeQuotes(tempDiv);
return textNodeQuotes.length > 0;
@@ -123,4 +131,26 @@ export class EmailQuoteExtractor {
return null;
}
/**
* Remove fallback blockquote if it is the last top-level element.
* @param {Element} rootElement - Root element containing the HTML
*/
static removeTrailingBlockquote(rootElement) {
const trailingBlockquote = this.findTrailingBlockquote(rootElement);
trailingBlockquote?.remove();
}
/**
* Locate a fallback blockquote that is the last top-level element.
* @param {Element} rootElement - Root element containing the HTML
* @returns {Element|null} The trailing blockquote element if present
*/
static findTrailingBlockquote(rootElement) {
const lastElement = rootElement.lastElementChild;
if (lastElement?.matches?.(BLOCKQUOTE_FALLBACK_SELECTOR)) {
return lastElement;
}
return null;
}
}

View File

@@ -68,13 +68,17 @@ export const registerSubscription = (onSuccess = () => {}) => {
.then(() => {
onSuccess();
})
.catch(() => {
.catch(error => {
// eslint-disable-next-line no-console
console.error('Push subscription registration failed:', error);
useAlert('This browser does not support desktop notification');
});
};
export const requestPushPermissions = ({ onSuccess }) => {
if (!('Notification' in window)) {
// eslint-disable-next-line no-console
console.warn('Notification is not supported');
useAlert('This browser does not support desktop notification');
} else if (Notification.permission === 'granted') {
registerSubscription(onSuccess);

View File

@@ -0,0 +1,332 @@
import { format, parseISO, isValid as isValidDate } from 'date-fns';
/**
* Extracts plain text from HTML content
* @param {string} html - HTML content to convert
* @returns {string} Plain text content
*/
export const extractPlainTextFromHtml = html => {
if (!html) {
return '';
}
if (typeof document === 'undefined') {
return html.replace(/<[^>]*>/g, ' ');
}
const tempDiv = document.createElement('div');
tempDiv.innerHTML = html;
return tempDiv.textContent || tempDiv.innerText || '';
};
/**
* Extracts sender name from email message
* @param {Object} lastEmail - Last email message object
* @param {Object} contact - Contact object
* @returns {string} Sender name
*/
export const getEmailSenderName = (lastEmail, contact) => {
const senderName = lastEmail?.sender?.name;
if (senderName && senderName.trim()) {
return senderName.trim();
}
const contactName = contact?.name;
return contactName && contactName.trim() ? contactName.trim() : '';
};
/**
* Extracts sender email from email message
* @param {Object} lastEmail - Last email message object
* @param {Object} contact - Contact object
* @returns {string} Sender email address
*/
export const getEmailSenderEmail = (lastEmail, contact) => {
const senderEmail = lastEmail?.sender?.email;
if (senderEmail && senderEmail.trim()) {
return senderEmail.trim();
}
const contentAttributes =
lastEmail?.contentAttributes || lastEmail?.content_attributes || {};
const emailMeta = contentAttributes.email || {};
if (Array.isArray(emailMeta.from) && emailMeta.from.length > 0) {
const fromAddress = emailMeta.from[0];
if (fromAddress && fromAddress.trim()) {
return fromAddress.trim();
}
}
const contactEmail = contact?.email;
return contactEmail && contactEmail.trim() ? contactEmail.trim() : '';
};
/**
* Extracts date from email message
* @param {Object} lastEmail - Last email message object
* @returns {Date|null} Email date
*/
export const getEmailDate = lastEmail => {
const contentAttributes =
lastEmail?.contentAttributes || lastEmail?.content_attributes || {};
const emailMeta = contentAttributes.email || {};
if (emailMeta.date) {
const parsedDate = parseISO(emailMeta.date);
if (isValidDate(parsedDate)) {
return parsedDate;
}
}
const createdAt = lastEmail?.created_at;
if (createdAt) {
const timestamp = Number(createdAt);
if (!Number.isNaN(timestamp)) {
const milliseconds = timestamp > 1e12 ? timestamp : timestamp * 1000;
const derivedDate = new Date(milliseconds);
if (!Number.isNaN(derivedDate.getTime())) {
return derivedDate;
}
}
}
return null;
};
/**
* Formats date for quoted email header
* @param {Date} date - Date to format
* @returns {string} Formatted date string
*/
export const formatQuotedEmailDate = date => {
try {
return format(date, "EEE, MMM d, yyyy 'at' p");
} catch (error) {
const fallbackDate = new Date(date);
if (!Number.isNaN(fallbackDate.getTime())) {
return format(fallbackDate, "EEE, MMM d, yyyy 'at' p");
}
}
return '';
};
/**
* Extracts inbox email address from last email message
* @param {Object} lastEmail - Last email message object
* @param {Object} inbox - Inbox object
* @returns {string} Inbox email address
*/
export const getInboxEmail = (lastEmail, inbox) => {
const contentAttributes =
lastEmail?.contentAttributes || lastEmail?.content_attributes || {};
const emailMeta = contentAttributes.email || {};
if (Array.isArray(emailMeta.to) && emailMeta.to.length > 0) {
const toAddress = emailMeta.to[0];
if (toAddress && toAddress.trim()) {
return toAddress.trim();
}
}
const inboxEmail = inbox?.email;
return inboxEmail && inboxEmail.trim() ? inboxEmail.trim() : '';
};
/**
* Builds quoted email header from contact (for incoming messages)
* @param {Object} lastEmail - Last email message object
* @param {Object} contact - Contact object
* @returns {string} Formatted header string
*/
export const buildQuotedEmailHeaderFromContact = (lastEmail, contact) => {
if (!lastEmail) {
return '';
}
const quotedDate = getEmailDate(lastEmail);
const senderEmail = getEmailSenderEmail(lastEmail, contact);
if (!quotedDate || !senderEmail) {
return '';
}
const formattedDate = formatQuotedEmailDate(quotedDate);
if (!formattedDate) {
return '';
}
const senderName = getEmailSenderName(lastEmail, contact);
const hasName = !!senderName;
const contactLabel = hasName
? `${senderName} <${senderEmail}>`
: `<${senderEmail}>`;
return `On ${formattedDate} ${contactLabel} wrote:`;
};
/**
* Builds quoted email header from inbox (for outgoing messages)
* @param {Object} lastEmail - Last email message object
* @param {Object} inbox - Inbox object
* @returns {string} Formatted header string
*/
export const buildQuotedEmailHeaderFromInbox = (lastEmail, inbox) => {
if (!lastEmail) {
return '';
}
const quotedDate = getEmailDate(lastEmail);
const inboxEmail = getInboxEmail(lastEmail, inbox);
if (!quotedDate || !inboxEmail) {
return '';
}
const formattedDate = formatQuotedEmailDate(quotedDate);
if (!formattedDate) {
return '';
}
const inboxName = inbox?.name;
const hasName = !!inboxName;
const inboxLabel = hasName
? `${inboxName} <${inboxEmail}>`
: `<${inboxEmail}>`;
return `On ${formattedDate} ${inboxLabel} wrote:`;
};
/**
* Builds quoted email header based on message type
* @param {Object} lastEmail - Last email message object
* @param {Object} contact - Contact object
* @param {Object} inbox - Inbox object
* @returns {string} Formatted header string
*/
export const buildQuotedEmailHeader = (lastEmail, contact, inbox) => {
if (!lastEmail) {
return '';
}
// MESSAGE_TYPE.OUTGOING = 1, MESSAGE_TYPE.INCOMING = 0
const isOutgoing = lastEmail.message_type === 1;
if (isOutgoing) {
return buildQuotedEmailHeaderFromInbox(lastEmail, inbox);
}
return buildQuotedEmailHeaderFromContact(lastEmail, contact);
};
/**
* Formats text as markdown blockquote
* @param {string} text - Text to format
* @param {string} header - Optional header to prepend
* @returns {string} Formatted blockquote
*/
export const formatQuotedTextAsBlockquote = (text, header = '') => {
const normalizedLines = text
? String(text).replace(/\r\n/g, '\n').split('\n')
: [];
if (!header && !normalizedLines.length) {
return '';
}
const quotedLines = [];
if (header) {
quotedLines.push(`> ${header}`);
quotedLines.push('>');
}
normalizedLines.forEach(line => {
const trimmedLine = line.trimEnd();
quotedLines.push(trimmedLine ? `> ${trimmedLine}` : '>');
});
return quotedLines.join('\n');
};
/**
* Extracts quoted email text from last email message
* @param {Object} lastEmail - Last email message object
* @returns {string} Quoted email text
*/
export const extractQuotedEmailText = lastEmail => {
if (!lastEmail) {
return '';
}
const contentAttributes =
lastEmail.contentAttributes || lastEmail.content_attributes || {};
const emailContent = contentAttributes.email || {};
const textContent = emailContent.textContent || emailContent.text_content;
if (textContent?.reply) {
return textContent.reply;
}
if (textContent?.full) {
return textContent.full;
}
const htmlContent = emailContent.htmlContent || emailContent.html_content;
if (htmlContent?.reply) {
return extractPlainTextFromHtml(htmlContent.reply);
}
if (htmlContent?.full) {
return extractPlainTextFromHtml(htmlContent.full);
}
const fallbackContent =
lastEmail.content || lastEmail.processed_message_content || '';
return fallbackContent;
};
/**
* Truncates text for preview display
* @param {string} text - Text to truncate
* @param {number} maxLength - Maximum length (default: 80)
* @returns {string} Truncated text
*/
export const truncatePreviewText = (text, maxLength = 80) => {
const preview = text.trim().replace(/\s+/g, ' ');
if (!preview) {
return '';
}
if (preview.length <= maxLength) {
return preview;
}
return `${preview.slice(0, maxLength - 3)}...`;
};
/**
* Appends quoted text to message
* @param {string} message - Original message
* @param {string} quotedText - Text to quote
* @param {string} header - Quote header
* @returns {string} Message with quoted text appended
*/
export const appendQuotedTextToMessage = (message, quotedText, header) => {
const baseMessage = message ? String(message) : '';
const quotedBlock = formatQuotedTextAsBlockquote(quotedText, header);
if (!quotedBlock) {
return baseMessage;
}
if (!baseMessage) {
return quotedBlock;
}
let separator = '\n\n';
if (baseMessage.endsWith('\n\n')) {
separator = '';
} else if (baseMessage.endsWith('\n')) {
separator = '\n';
}
return `${baseMessage}${separator}${quotedBlock}`;
};

View File

@@ -0,0 +1,99 @@
import { describe, it, expect } from 'vitest';
import { EmailQuoteExtractor } from '../emailQuoteExtractor.js';
const SAMPLE_EMAIL_HTML = `
<p>method</p>
<blockquote>
<p>On Mon, Sep 29, 2025 at 5:18 PM John <a href="mailto:shivam@chatwoot.com">shivam@chatwoot.com</a> wrote:</p>
<p>Hi</p>
<blockquote>
<p>On Mon, Sep 29, 2025 at 5:17 PM Shivam Mishra <a href="mailto:shivam@chatwoot.com">shivam@chatwoot.com</a> wrote:</p>
<p>Yes, it is.</p>
<p>On Mon, Sep 29, 2025 at 5:16 PM John from Shaneforwoot &lt; shaneforwoot@gmail.com&gt; wrote:</p>
<blockquote>
<p>Hey</p>
<p>On Mon, Sep 29, 2025 at 4:59 PM John shivam@chatwoot.com wrote:</p>
<p>This is another quoted quoted text reply</p>
<p>This is nice</p>
<p>On Mon, Sep 29, 2025 at 4:21 PM John from Shaneforwoot &lt; &gt; shaneforwoot@gmail.com&gt; wrote:</p>
<p>Hey there, this is a reply from Chatwoot, notice the quoted text</p>
<p>Hey there</p>
<p>This is an email text, enjoy reading this</p>
<p>-- Shivam Mishra, Chatwoot</p>
</blockquote>
</blockquote>
</blockquote>
`;
const EMAIL_WITH_SIGNATURE = `
<p>Latest reply here.</p>
<p>Thanks,</p>
<p>Jane Doe</p>
<blockquote>
<p>On Mon, Sep 22, Someone wrote:</p>
<p>Previous reply content</p>
</blockquote>
`;
const EMAIL_WITH_FOLLOW_UP_CONTENT = `
<blockquote>
<p>Inline quote that should stay</p>
</blockquote>
<p>Internal note follows</p>
<p>Regards,</p>
`;
describe('EmailQuoteExtractor', () => {
it('removes blockquote-based quotes from the email body', () => {
const cleanedHtml = EmailQuoteExtractor.extractQuotes(SAMPLE_EMAIL_HTML);
const container = document.createElement('div');
container.innerHTML = cleanedHtml;
expect(container.querySelectorAll('blockquote').length).toBe(0);
expect(container.textContent?.trim()).toBe('method');
expect(container.textContent).not.toContain(
'On Mon, Sep 29, 2025 at 5:18 PM'
);
});
it('keeps blockquote fallback when it is not the last top-level element', () => {
const cleanedHtml = EmailQuoteExtractor.extractQuotes(
EMAIL_WITH_FOLLOW_UP_CONTENT
);
const container = document.createElement('div');
container.innerHTML = cleanedHtml;
expect(container.querySelector('blockquote')).not.toBeNull();
expect(container.lastElementChild?.tagName).toBe('P');
});
it('detects quote indicators in nested blockquotes', () => {
const result = EmailQuoteExtractor.hasQuotes(SAMPLE_EMAIL_HTML);
expect(result).toBe(true);
});
it('does not flag blockquotes that are followed by other elements', () => {
expect(EmailQuoteExtractor.hasQuotes(EMAIL_WITH_FOLLOW_UP_CONTENT)).toBe(
false
);
});
it('returns false when no quote indicators are present', () => {
const html = '<p>Plain content</p>';
expect(EmailQuoteExtractor.hasQuotes(html)).toBe(false);
});
it('removes trailing blockquotes while preserving trailing signatures', () => {
const cleanedHtml = EmailQuoteExtractor.extractQuotes(EMAIL_WITH_SIGNATURE);
expect(cleanedHtml).toContain('<p>Thanks,</p>');
expect(cleanedHtml).toContain('<p>Jane Doe</p>');
expect(cleanedHtml).not.toContain('<blockquote');
});
it('detects quotes for trailing blockquotes even when signatures follow text', () => {
expect(EmailQuoteExtractor.hasQuotes(EMAIL_WITH_SIGNATURE)).toBe(true);
});
});

View File

@@ -0,0 +1,441 @@
import {
extractPlainTextFromHtml,
getEmailSenderName,
getEmailSenderEmail,
getEmailDate,
formatQuotedEmailDate,
getInboxEmail,
buildQuotedEmailHeader,
buildQuotedEmailHeaderFromContact,
buildQuotedEmailHeaderFromInbox,
formatQuotedTextAsBlockquote,
extractQuotedEmailText,
truncatePreviewText,
appendQuotedTextToMessage,
} from '../quotedEmailHelper';
describe('quotedEmailHelper', () => {
describe('extractPlainTextFromHtml', () => {
it('returns empty string for null or undefined', () => {
expect(extractPlainTextFromHtml(null)).toBe('');
expect(extractPlainTextFromHtml(undefined)).toBe('');
});
it('strips HTML tags and returns plain text', () => {
const html = '<p>Hello <strong>world</strong></p>';
const result = extractPlainTextFromHtml(html);
expect(result).toBe('Hello world');
});
it('handles complex HTML structure', () => {
const html = '<div><p>Line 1</p><p>Line 2</p></div>';
const result = extractPlainTextFromHtml(html);
expect(result).toContain('Line 1');
expect(result).toContain('Line 2');
});
});
describe('getEmailSenderName', () => {
it('returns sender name from lastEmail', () => {
const lastEmail = { sender: { name: 'John Doe' } };
const result = getEmailSenderName(lastEmail, {});
expect(result).toBe('John Doe');
});
it('returns contact name if sender name not available', () => {
const lastEmail = { sender: {} };
const contact = { name: 'Jane Smith' };
const result = getEmailSenderName(lastEmail, contact);
expect(result).toBe('Jane Smith');
});
it('returns empty string if neither available', () => {
const result = getEmailSenderName({}, {});
expect(result).toBe('');
});
it('trims whitespace from names', () => {
const lastEmail = { sender: { name: ' John Doe ' } };
const result = getEmailSenderName(lastEmail, {});
expect(result).toBe('John Doe');
});
});
describe('getEmailSenderEmail', () => {
it('returns sender email from lastEmail', () => {
const lastEmail = { sender: { email: 'john@example.com' } };
const result = getEmailSenderEmail(lastEmail, {});
expect(result).toBe('john@example.com');
});
it('returns email from contentAttributes if sender email not available', () => {
const lastEmail = {
contentAttributes: {
email: { from: ['jane@example.com'] },
},
};
const result = getEmailSenderEmail(lastEmail, {});
expect(result).toBe('jane@example.com');
});
it('returns contact email as fallback', () => {
const lastEmail = {};
const contact = { email: 'contact@example.com' };
const result = getEmailSenderEmail(lastEmail, contact);
expect(result).toBe('contact@example.com');
});
it('trims whitespace from emails', () => {
const lastEmail = { sender: { email: ' john@example.com ' } };
const result = getEmailSenderEmail(lastEmail, {});
expect(result).toBe('john@example.com');
});
});
describe('getEmailDate', () => {
it('returns parsed date from email metadata', () => {
const lastEmail = {
contentAttributes: {
email: { date: '2024-01-15T10:30:00Z' },
},
};
const result = getEmailDate(lastEmail);
expect(result).toBeInstanceOf(Date);
});
it('returns date from created_at timestamp', () => {
const lastEmail = { created_at: 1705318200 };
const result = getEmailDate(lastEmail);
expect(result).toBeInstanceOf(Date);
});
it('handles millisecond timestamps', () => {
const lastEmail = { created_at: 1705318200000 };
const result = getEmailDate(lastEmail);
expect(result).toBeInstanceOf(Date);
});
it('returns null if no valid date found', () => {
const result = getEmailDate({});
expect(result).toBeNull();
});
});
describe('formatQuotedEmailDate', () => {
it('formats date correctly', () => {
const date = new Date('2024-01-15T10:30:00Z');
const result = formatQuotedEmailDate(date);
expect(result).toMatch(/Mon, Jan 15, 2024 at/);
});
it('returns empty string for invalid date', () => {
const result = formatQuotedEmailDate('invalid');
expect(result).toBe('');
});
});
describe('getInboxEmail', () => {
it('returns email from contentAttributes.email.to', () => {
const lastEmail = {
contentAttributes: {
email: { to: ['inbox@example.com'] },
},
};
const result = getInboxEmail(lastEmail, {});
expect(result).toBe('inbox@example.com');
});
it('returns inbox email as fallback', () => {
const lastEmail = {};
const inbox = { email: 'support@example.com' };
const result = getInboxEmail(lastEmail, inbox);
expect(result).toBe('support@example.com');
});
it('returns empty string if no email found', () => {
expect(getInboxEmail({}, {})).toBe('');
});
it('trims whitespace from emails', () => {
const lastEmail = {
contentAttributes: {
email: { to: [' inbox@example.com '] },
},
};
const result = getInboxEmail(lastEmail, {});
expect(result).toBe('inbox@example.com');
});
});
describe('buildQuotedEmailHeaderFromContact', () => {
it('builds complete header with name and email', () => {
const lastEmail = {
sender: { name: 'John Doe', email: 'john@example.com' },
contentAttributes: {
email: { date: '2024-01-15T10:30:00Z' },
},
};
const result = buildQuotedEmailHeaderFromContact(lastEmail, {});
expect(result).toContain('John Doe');
expect(result).toContain('john@example.com');
expect(result).toContain('wrote:');
});
it('builds header without name if not available', () => {
const lastEmail = {
sender: { email: 'john@example.com' },
contentAttributes: {
email: { date: '2024-01-15T10:30:00Z' },
},
};
const result = buildQuotedEmailHeaderFromContact(lastEmail, {});
expect(result).toContain('<john@example.com>');
expect(result).not.toContain('undefined');
});
it('returns empty string if missing required data', () => {
expect(buildQuotedEmailHeaderFromContact(null, {})).toBe('');
expect(buildQuotedEmailHeaderFromContact({}, {})).toBe('');
});
});
describe('buildQuotedEmailHeaderFromInbox', () => {
it('builds complete header with inbox name and email', () => {
const lastEmail = {
contentAttributes: {
email: {
date: '2024-01-15T10:30:00Z',
to: ['support@example.com'],
},
},
};
const inbox = { name: 'Support Team', email: 'support@example.com' };
const result = buildQuotedEmailHeaderFromInbox(lastEmail, inbox);
expect(result).toContain('Support Team');
expect(result).toContain('support@example.com');
expect(result).toContain('wrote:');
});
it('builds header without name if not available', () => {
const lastEmail = {
contentAttributes: {
email: {
date: '2024-01-15T10:30:00Z',
to: ['inbox@example.com'],
},
},
};
const inbox = { email: 'inbox@example.com' };
const result = buildQuotedEmailHeaderFromInbox(lastEmail, inbox);
expect(result).toContain('<inbox@example.com>');
expect(result).not.toContain('undefined');
});
it('returns empty string if missing required data', () => {
expect(buildQuotedEmailHeaderFromInbox(null, {})).toBe('');
expect(buildQuotedEmailHeaderFromInbox({}, {})).toBe('');
});
});
describe('buildQuotedEmailHeader', () => {
it('uses inbox email for outgoing messages (message_type: 1)', () => {
const lastEmail = {
message_type: 1,
contentAttributes: {
email: {
date: '2024-01-15T10:30:00Z',
to: ['support@example.com'],
},
},
};
const inbox = { name: 'Support', email: 'support@example.com' };
const contact = { name: 'John Doe', email: 'john@example.com' };
const result = buildQuotedEmailHeader(lastEmail, contact, inbox);
expect(result).toContain('Support');
expect(result).toContain('support@example.com');
expect(result).not.toContain('John Doe');
});
it('uses contact email for incoming messages (message_type: 0)', () => {
const lastEmail = {
message_type: 0,
sender: { name: 'Jane Smith', email: 'jane@example.com' },
contentAttributes: {
email: { date: '2024-01-15T10:30:00Z' },
},
};
const inbox = { name: 'Support', email: 'support@example.com' };
const contact = { name: 'Jane Smith', email: 'jane@example.com' };
const result = buildQuotedEmailHeader(lastEmail, contact, inbox);
expect(result).toContain('Jane Smith');
expect(result).toContain('jane@example.com');
expect(result).not.toContain('Support');
});
it('returns empty string if missing required data', () => {
expect(buildQuotedEmailHeader(null, {}, {})).toBe('');
expect(buildQuotedEmailHeader({}, {}, {})).toBe('');
});
});
describe('formatQuotedTextAsBlockquote', () => {
it('formats single line text', () => {
const result = formatQuotedTextAsBlockquote('Hello world');
expect(result).toBe('> Hello world');
});
it('formats multi-line text', () => {
const text = 'Line 1\nLine 2\nLine 3';
const result = formatQuotedTextAsBlockquote(text);
expect(result).toBe('> Line 1\n> Line 2\n> Line 3');
});
it('includes header if provided', () => {
const result = formatQuotedTextAsBlockquote('Hello', 'Header text');
expect(result).toContain('> Header text');
expect(result).toContain('>\n> Hello');
});
it('handles empty lines correctly', () => {
const text = 'Line 1\n\nLine 3';
const result = formatQuotedTextAsBlockquote(text);
expect(result).toBe('> Line 1\n>\n> Line 3');
});
it('returns empty string for empty input', () => {
expect(formatQuotedTextAsBlockquote('')).toBe('');
expect(formatQuotedTextAsBlockquote('', '')).toBe('');
});
it('handles Windows line endings', () => {
const text = 'Line 1\r\nLine 2';
const result = formatQuotedTextAsBlockquote(text);
expect(result).toBe('> Line 1\n> Line 2');
});
});
describe('extractQuotedEmailText', () => {
it('extracts text from textContent.reply', () => {
const lastEmail = {
contentAttributes: {
email: { textContent: { reply: 'Reply text' } },
},
};
const result = extractQuotedEmailText(lastEmail);
expect(result).toBe('Reply text');
});
it('falls back to textContent.full', () => {
const lastEmail = {
contentAttributes: {
email: { textContent: { full: 'Full text' } },
},
};
const result = extractQuotedEmailText(lastEmail);
expect(result).toBe('Full text');
});
it('extracts from htmlContent and converts to plain text', () => {
const lastEmail = {
contentAttributes: {
email: { htmlContent: { reply: '<p>HTML reply</p>' } },
},
};
const result = extractQuotedEmailText(lastEmail);
expect(result).toBe('HTML reply');
});
it('uses fallback content if structured content not available', () => {
const lastEmail = { content: 'Fallback content' };
const result = extractQuotedEmailText(lastEmail);
expect(result).toBe('Fallback content');
});
it('returns empty string for null or missing email', () => {
expect(extractQuotedEmailText(null)).toBe('');
expect(extractQuotedEmailText({})).toBe('');
});
});
describe('truncatePreviewText', () => {
it('returns full text if under max length', () => {
const text = 'Short text';
const result = truncatePreviewText(text, 80);
expect(result).toBe('Short text');
});
it('truncates text exceeding max length', () => {
const text = 'A'.repeat(100);
const result = truncatePreviewText(text, 80);
expect(result).toHaveLength(80);
expect(result).toContain('...');
});
it('collapses multiple spaces', () => {
const text = 'Text with spaces';
const result = truncatePreviewText(text);
expect(result).toBe('Text with spaces');
});
it('trims whitespace', () => {
const text = ' Text with spaces ';
const result = truncatePreviewText(text);
expect(result).toBe('Text with spaces');
});
it('returns empty string for empty input', () => {
expect(truncatePreviewText('')).toBe('');
expect(truncatePreviewText(' ')).toBe('');
});
it('uses default max length of 80', () => {
const text = 'A'.repeat(100);
const result = truncatePreviewText(text);
expect(result).toHaveLength(80);
});
});
describe('appendQuotedTextToMessage', () => {
it('appends quoted text to message', () => {
const message = 'My reply';
const quotedText = 'Original message';
const header = 'On date sender wrote:';
const result = appendQuotedTextToMessage(message, quotedText, header);
expect(result).toContain('My reply');
expect(result).toContain('> On date sender wrote:');
expect(result).toContain('> Original message');
});
it('returns only quoted text if message is empty', () => {
const result = appendQuotedTextToMessage('', 'Quoted', 'Header');
expect(result).toContain('> Header');
expect(result).toContain('> Quoted');
expect(result).not.toContain('\n\n\n');
});
it('returns message if no quoted text', () => {
const result = appendQuotedTextToMessage('Message', '', '');
expect(result).toBe('Message');
});
it('handles proper spacing with double newline', () => {
const result = appendQuotedTextToMessage('Message', 'Quoted', 'Header');
expect(result).toContain('Message\n\n>');
});
it('does not add extra newlines if message already ends with newlines', () => {
const result = appendQuotedTextToMessage(
'Message\n\n',
'Quoted',
'Header'
);
expect(result).not.toContain('\n\n\n');
});
it('adds single newline if message ends with one newline', () => {
const result = appendQuotedTextToMessage('Message\n', 'Quoted', 'Header');
expect(result).toContain('Message\n\n>');
});
});
});

View File

@@ -227,6 +227,13 @@
"YES": "Send",
"CANCEL": "Cancel"
}
},
"QUOTED_REPLY": {
"ENABLE_TOOLTIP": "Include quoted email thread",
"DISABLE_TOOLTIP": "Don't include quoted email thread",
"REMOVE_PREVIEW": "Remove quoted email thread",
"COLLAPSE": "Collapse preview",
"EXPAND": "Expand preview"
}
},
"VISIBLE_TO_AGENTS": "Private Note: Only visible to you and your team",

View File

@@ -5,6 +5,8 @@
"PLACEHOLDER": "Search",
"EMPTY_STATE": "No results found"
},
"CLOSE": "Close"
"CLOSE": "Close",
"BETA": "Beta",
"BETA_DESCRIPTION": "This feature is in beta and may change as we improve it."
}
}

View File

@@ -618,6 +618,11 @@
"SETTINGS_POPUP": {
"MESSENGER_HEADING": "Messenger Script",
"MESSENGER_SUB_HEAD": "Place this button inside your body tag",
"ALLOWED_DOMAINS": {
"TITLE": "Allowed Domains",
"SUBTITLE": "Add wildcard or regular domains separated by commas (leave blank to allow all), e.g. *.chatwoot.dev, chatwoot.com.",
"PLACEHOLDER": "Enter domains separated by commas (eg: *.chatwoot.dev, chatwoot.com)"
},
"INBOX_AGENTS": "Agents",
"INBOX_AGENTS_SUB_TEXT": "Add or remove agents from this inbox",
"AGENT_ASSIGNMENT": "Conversation Assignment",

View File

@@ -24,7 +24,7 @@
"CREATE_NEW_ACCOUNT": "Create a new account",
"SUBMIT": "Login",
"SAML": {
"LABEL": "Log in via SSO",
"LABEL": "Login via SSO",
"TITLE": "Initiate Single Sign-on (SSO)",
"SUBTITLE": "Enter your work email to access your organization",
"BACK_TO_LOGIN": "Login via Password",
@@ -34,7 +34,7 @@
},
"SUBMIT": "Continue with SSO",
"API": {
"ERROR_MESSAGE": "SSO authentication failed"
"ERROR_MESSAGE": "SSO authentication failed. Please check your credentials and try again."
}
}
}

View File

@@ -27,15 +27,20 @@
"LABEL": "Password",
"PLACEHOLDER": "Password",
"ERROR": "Password is too short.",
"IS_INVALID_PASSWORD": "Password should contain atleast 1 uppercase letter, 1 lowercase letter, 1 number and 1 special character."
"IS_INVALID_PASSWORD": "Password should contain atleast 1 uppercase letter, 1 lowercase letter, 1 number and 1 special character.",
"REQUIREMENTS_LENGTH": "At least 6 characters long",
"REQUIREMENTS_UPPERCASE": "At least one uppercase letter",
"REQUIREMENTS_LOWERCASE": "At least one lowercase letter",
"REQUIREMENTS_NUMBER": "At least one number",
"REQUIREMENTS_SPECIAL": "At least one special character"
},
"CONFIRM_PASSWORD": {
"LABEL": "Confirm password",
"PLACEHOLDER": "Confirm password",
"ERROR": "Password doesnot match."
"ERROR": "Passwords do not match."
},
"API": {
"SUCCESS_MESSAGE": "Registration Successfull",
"SUCCESS_MESSAGE": "Registration Successful",
"ERROR_MESSAGE": "Could not connect to Woot server. Please try again."
},
"SUBMIT": "Create account",

View File

@@ -227,6 +227,13 @@
"YES": "إرسال",
"CANCEL": "إلغاء"
}
},
"QUOTED_REPLY": {
"ENABLE_TOOLTIP": "Include quoted email thread",
"DISABLE_TOOLTIP": "Don't include quoted email thread",
"REMOVE_PREVIEW": "Remove quoted email thread",
"COLLAPSE": "Collapse preview",
"EXPAND": "Expand preview"
}
},
"VISIBLE_TO_AGENTS": "ملاحظة خاصة: مرئية فقط لك ولأعضاء فريقك",

View File

@@ -5,6 +5,8 @@
"PLACEHOLDER": "بحث",
"EMPTY_STATE": "لم يتم العثور على النتائج"
},
"CLOSE": "أغلق"
"CLOSE": "أغلق",
"BETA": "تجريبي",
"BETA_DESCRIPTION": "This feature is in beta and may change as we improve it."
}
}

View File

@@ -618,6 +618,11 @@
"SETTINGS_POPUP": {
"MESSENGER_HEADING": "كود \"الماسنجر\"",
"MESSENGER_SUB_HEAD": "ضع هذا الكود داخل وسم الـ body في موقعك",
"ALLOWED_DOMAINS": {
"TITLE": "Allowed Domains",
"SUBTITLE": "Add wildcard or regular domains separated by commas (leave blank to allow all), e.g. *.chatwoot.dev, chatwoot.com.",
"PLACEHOLDER": "Enter domains separated by commas (eg: *.chatwoot.dev, chatwoot.com)"
},
"INBOX_AGENTS": "وكيل الدعم",
"INBOX_AGENTS_SUB_TEXT": "إضافة أو إزالة وكلاء من صندوق الوارد هذا",
"AGENT_ASSIGNMENT": "تعيين المحادثة",

View File

@@ -24,7 +24,7 @@
"CREATE_NEW_ACCOUNT": "إنشاء حساب جديد",
"SUBMIT": "تسجيل الدخول",
"SAML": {
"LABEL": "Log in via SSO",
"LABEL": "Login via SSO",
"TITLE": "Initiate Single Sign-on (SSO)",
"SUBTITLE": "Enter your work email to access your organization",
"BACK_TO_LOGIN": "Login via Password",
@@ -34,7 +34,7 @@
},
"SUBMIT": "Continue with SSO",
"API": {
"ERROR_MESSAGE": "SSO authentication failed"
"ERROR_MESSAGE": "SSO authentication failed. Please check your credentials and try again."
}
}
}

View File

@@ -27,15 +27,20 @@
"LABEL": "كلمة المرور",
"PLACEHOLDER": "كلمة المرور",
"ERROR": "كلمة المرور قصيرة جداً",
"IS_INVALID_PASSWORD": "يجب أن تحتوي كلمة المرور على الأقل على حرف كبير واحد وحرف صغير واحد ورقم واحد وحرف خاص واحد"
"IS_INVALID_PASSWORD": "يجب أن تحتوي كلمة المرور على الأقل على حرف كبير واحد وحرف صغير واحد ورقم واحد وحرف خاص واحد",
"REQUIREMENTS_LENGTH": "At least 6 characters long",
"REQUIREMENTS_UPPERCASE": "At least one uppercase letter",
"REQUIREMENTS_LOWERCASE": "At least one lowercase letter",
"REQUIREMENTS_NUMBER": "At least one number",
"REQUIREMENTS_SPECIAL": "At least one special character"
},
"CONFIRM_PASSWORD": {
"LABEL": "تأكيد كلمة المرور",
"PLACEHOLDER": "تأكيد كلمة المرور",
"ERROR": "كلمة المرور غير متطابقة"
"ERROR": "كلمة المرور غير متطابقة."
},
"API": {
"SUCCESS_MESSAGE": "تم التسجيل بنجاح",
"SUCCESS_MESSAGE": "Registration Successful",
"ERROR_MESSAGE": "تعذر الاتصال بالخادم، الرجاء المحاولة مرة أخرى لاحقاً"
},
"SUBMIT": "إرسال",

View File

@@ -227,6 +227,13 @@
"YES": "Send",
"CANCEL": "Cancel"
}
},
"QUOTED_REPLY": {
"ENABLE_TOOLTIP": "Include quoted email thread",
"DISABLE_TOOLTIP": "Don't include quoted email thread",
"REMOVE_PREVIEW": "Remove quoted email thread",
"COLLAPSE": "Collapse preview",
"EXPAND": "Expand preview"
}
},
"VISIBLE_TO_AGENTS": "Private Note: Only visible to you and your team",

View File

@@ -5,6 +5,8 @@
"PLACEHOLDER": "Search",
"EMPTY_STATE": "No results found"
},
"CLOSE": "Close"
"CLOSE": "Close",
"BETA": "Beta",
"BETA_DESCRIPTION": "This feature is in beta and may change as we improve it."
}
}

View File

@@ -618,6 +618,11 @@
"SETTINGS_POPUP": {
"MESSENGER_HEADING": "Messenger Script",
"MESSENGER_SUB_HEAD": "Place this button inside your body tag",
"ALLOWED_DOMAINS": {
"TITLE": "Allowed Domains",
"SUBTITLE": "Add wildcard or regular domains separated by commas (leave blank to allow all), e.g. *.chatwoot.dev, chatwoot.com.",
"PLACEHOLDER": "Enter domains separated by commas (eg: *.chatwoot.dev, chatwoot.com)"
},
"INBOX_AGENTS": "Agents",
"INBOX_AGENTS_SUB_TEXT": "Add or remove agents from this inbox",
"AGENT_ASSIGNMENT": "Conversation Assignment",

View File

@@ -24,7 +24,7 @@
"CREATE_NEW_ACCOUNT": "Create a new account",
"SUBMIT": "Login",
"SAML": {
"LABEL": "Log in via SSO",
"LABEL": "Login via SSO",
"TITLE": "Initiate Single Sign-on (SSO)",
"SUBTITLE": "Enter your work email to access your organization",
"BACK_TO_LOGIN": "Login via Password",
@@ -34,7 +34,7 @@
},
"SUBMIT": "Continue with SSO",
"API": {
"ERROR_MESSAGE": "SSO authentication failed"
"ERROR_MESSAGE": "SSO authentication failed. Please check your credentials and try again."
}
}
}

View File

@@ -27,15 +27,20 @@
"LABEL": "Password",
"PLACEHOLDER": "Password",
"ERROR": "Password is too short.",
"IS_INVALID_PASSWORD": "Password should contain atleast 1 uppercase letter, 1 lowercase letter, 1 number and 1 special character."
"IS_INVALID_PASSWORD": "Password should contain atleast 1 uppercase letter, 1 lowercase letter, 1 number and 1 special character.",
"REQUIREMENTS_LENGTH": "At least 6 characters long",
"REQUIREMENTS_UPPERCASE": "At least one uppercase letter",
"REQUIREMENTS_LOWERCASE": "At least one lowercase letter",
"REQUIREMENTS_NUMBER": "At least one number",
"REQUIREMENTS_SPECIAL": "At least one special character"
},
"CONFIRM_PASSWORD": {
"LABEL": "Confirm password",
"PLACEHOLDER": "Confirm password",
"ERROR": "Password doesnot match."
"ERROR": "Passwords do not match."
},
"API": {
"SUCCESS_MESSAGE": "Registration Successfull",
"SUCCESS_MESSAGE": "Registration Successful",
"ERROR_MESSAGE": "Could not connect to Woot server. Please try again."
},
"SUBMIT": "Create account",

View File

@@ -227,6 +227,13 @@
"YES": "Send",
"CANCEL": "Отмени"
}
},
"QUOTED_REPLY": {
"ENABLE_TOOLTIP": "Include quoted email thread",
"DISABLE_TOOLTIP": "Don't include quoted email thread",
"REMOVE_PREVIEW": "Remove quoted email thread",
"COLLAPSE": "Collapse preview",
"EXPAND": "Expand preview"
}
},
"VISIBLE_TO_AGENTS": "Private Note: Only visible to you and your team",

View File

@@ -5,6 +5,8 @@
"PLACEHOLDER": "Търсене",
"EMPTY_STATE": "Няма намерени резултати"
},
"CLOSE": "Close"
"CLOSE": "Close",
"BETA": "Beta",
"BETA_DESCRIPTION": "This feature is in beta and may change as we improve it."
}
}

View File

@@ -618,6 +618,11 @@
"SETTINGS_POPUP": {
"MESSENGER_HEADING": "Messenger Script",
"MESSENGER_SUB_HEAD": "Place this button inside your body tag",
"ALLOWED_DOMAINS": {
"TITLE": "Allowed Domains",
"SUBTITLE": "Add wildcard or regular domains separated by commas (leave blank to allow all), e.g. *.chatwoot.dev, chatwoot.com.",
"PLACEHOLDER": "Enter domains separated by commas (eg: *.chatwoot.dev, chatwoot.com)"
},
"INBOX_AGENTS": "Агенти",
"INBOX_AGENTS_SUB_TEXT": "Add or remove agents from this inbox",
"AGENT_ASSIGNMENT": "Conversation Assignment",

View File

@@ -24,7 +24,7 @@
"CREATE_NEW_ACCOUNT": "Create new account",
"SUBMIT": "Login",
"SAML": {
"LABEL": "Log in via SSO",
"LABEL": "Login via SSO",
"TITLE": "Initiate Single Sign-on (SSO)",
"SUBTITLE": "Enter your work email to access your organization",
"BACK_TO_LOGIN": "Login via Password",
@@ -34,7 +34,7 @@
},
"SUBMIT": "Continue with SSO",
"API": {
"ERROR_MESSAGE": "SSO authentication failed"
"ERROR_MESSAGE": "SSO authentication failed. Please check your credentials and try again."
}
}
}

View File

@@ -27,15 +27,20 @@
"LABEL": "Password",
"PLACEHOLDER": "Password",
"ERROR": "Password is too short",
"IS_INVALID_PASSWORD": "Password should contain atleast 1 uppercase letter, 1 lowercase letter, 1 number and 1 special character"
"IS_INVALID_PASSWORD": "Password should contain atleast 1 uppercase letter, 1 lowercase letter, 1 number and 1 special character",
"REQUIREMENTS_LENGTH": "At least 6 characters long",
"REQUIREMENTS_UPPERCASE": "At least one uppercase letter",
"REQUIREMENTS_LOWERCASE": "At least one lowercase letter",
"REQUIREMENTS_NUMBER": "At least one number",
"REQUIREMENTS_SPECIAL": "At least one special character"
},
"CONFIRM_PASSWORD": {
"LABEL": "Confirm Password",
"PLACEHOLDER": "Confirm Password",
"ERROR": "Password doesnot match"
"ERROR": "Passwords do not match."
},
"API": {
"SUCCESS_MESSAGE": "Registration Successfull",
"SUCCESS_MESSAGE": "Registration Successful",
"ERROR_MESSAGE": "Не можа да се свърже с Woot сървър. Моля, опитайте отново по-късно"
},
"SUBMIT": "Create account",

View File

@@ -227,6 +227,13 @@
"YES": "Envia",
"CANCEL": "Cancel·la"
}
},
"QUOTED_REPLY": {
"ENABLE_TOOLTIP": "Include quoted email thread",
"DISABLE_TOOLTIP": "Don't include quoted email thread",
"REMOVE_PREVIEW": "Remove quoted email thread",
"COLLAPSE": "Collapse preview",
"EXPAND": "Expand preview"
}
},
"VISIBLE_TO_AGENTS": "Nota privada: Només és visible per tu i el vostre equip",

View File

@@ -5,6 +5,8 @@
"PLACEHOLDER": "Cercar",
"EMPTY_STATE": "No s'ha trobat agents"
},
"CLOSE": "Tanca"
"CLOSE": "Tanca",
"BETA": "Beta",
"BETA_DESCRIPTION": "This feature is in beta and may change as we improve it."
}
}

View File

@@ -618,6 +618,11 @@
"SETTINGS_POPUP": {
"MESSENGER_HEADING": "Script del missatger",
"MESSENGER_SUB_HEAD": "Col·loca aquest botó dins de l'etiqueta body",
"ALLOWED_DOMAINS": {
"TITLE": "Allowed Domains",
"SUBTITLE": "Add wildcard or regular domains separated by commas (leave blank to allow all), e.g. *.chatwoot.dev, chatwoot.com.",
"PLACEHOLDER": "Enter domains separated by commas (eg: *.chatwoot.dev, chatwoot.com)"
},
"INBOX_AGENTS": "Agents",
"INBOX_AGENTS_SUB_TEXT": "Afegir o eliminar agents d'aquesta safata d'entrada",
"AGENT_ASSIGNMENT": "Conversació Assignada",

View File

@@ -24,7 +24,7 @@
"CREATE_NEW_ACCOUNT": "Crear un nou compte",
"SUBMIT": "Inicia la sessió",
"SAML": {
"LABEL": "Log in via SSO",
"LABEL": "Login via SSO",
"TITLE": "Initiate Single Sign-on (SSO)",
"SUBTITLE": "Enter your work email to access your organization",
"BACK_TO_LOGIN": "Login via Password",
@@ -34,7 +34,7 @@
},
"SUBMIT": "Continue with SSO",
"API": {
"ERROR_MESSAGE": "SSO authentication failed"
"ERROR_MESSAGE": "SSO authentication failed. Please check your credentials and try again."
}
}
}

View File

@@ -27,15 +27,20 @@
"LABEL": "Contrasenya",
"PLACEHOLDER": "Contrasenya",
"ERROR": "La contrasenya és massa curta",
"IS_INVALID_PASSWORD": "Password should contain atleast 1 uppercase letter, 1 lowercase letter, 1 number and 1 special character"
"IS_INVALID_PASSWORD": "Password should contain atleast 1 uppercase letter, 1 lowercase letter, 1 number and 1 special character",
"REQUIREMENTS_LENGTH": "At least 6 characters long",
"REQUIREMENTS_UPPERCASE": "At least one uppercase letter",
"REQUIREMENTS_LOWERCASE": "At least one lowercase letter",
"REQUIREMENTS_NUMBER": "At least one number",
"REQUIREMENTS_SPECIAL": "At least one special character"
},
"CONFIRM_PASSWORD": {
"LABEL": "Confirma la contrasenya",
"PLACEHOLDER": "Confirma la contrasenya",
"ERROR": "La contrasenya no coincideix."
"ERROR": "La contrasenya no coindeix."
},
"API": {
"SUCCESS_MESSAGE": "Registrat correctament",
"SUCCESS_MESSAGE": "Registration Successful",
"ERROR_MESSAGE": "No s'ha pogut connectar amb el servidor Woot. Torna-ho a provar més endavant"
},
"SUBMIT": "Crear un compte",

View File

@@ -227,6 +227,13 @@
"YES": "Poslat",
"CANCEL": "Zrušit"
}
},
"QUOTED_REPLY": {
"ENABLE_TOOLTIP": "Include quoted email thread",
"DISABLE_TOOLTIP": "Don't include quoted email thread",
"REMOVE_PREVIEW": "Remove quoted email thread",
"COLLAPSE": "Collapse preview",
"EXPAND": "Expand preview"
}
},
"VISIBLE_TO_AGENTS": "Soukromá poznámka: Viditelné pouze pro vás a váš tým",

View File

@@ -5,6 +5,8 @@
"PLACEHOLDER": "Hledat",
"EMPTY_STATE": "Žádné výsledky"
},
"CLOSE": "Zavřít"
"CLOSE": "Zavřít",
"BETA": "Beta",
"BETA_DESCRIPTION": "This feature is in beta and may change as we improve it."
}
}

View File

@@ -618,6 +618,11 @@
"SETTINGS_POPUP": {
"MESSENGER_HEADING": "Messenger skript",
"MESSENGER_SUB_HEAD": "Umístěte toto tlačítko dovnitř vašeho tělesného štítku",
"ALLOWED_DOMAINS": {
"TITLE": "Allowed Domains",
"SUBTITLE": "Add wildcard or regular domains separated by commas (leave blank to allow all), e.g. *.chatwoot.dev, chatwoot.com.",
"PLACEHOLDER": "Enter domains separated by commas (eg: *.chatwoot.dev, chatwoot.com)"
},
"INBOX_AGENTS": "Agenti",
"INBOX_AGENTS_SUB_TEXT": "Přidat nebo odebrat agenty z této složky doručené pošty",
"AGENT_ASSIGNMENT": "Conversation Assignment",

View File

@@ -24,7 +24,7 @@
"CREATE_NEW_ACCOUNT": "Vytvořit nový účet",
"SUBMIT": "Přihlásit se",
"SAML": {
"LABEL": "Log in via SSO",
"LABEL": "Login via SSO",
"TITLE": "Initiate Single Sign-on (SSO)",
"SUBTITLE": "Enter your work email to access your organization",
"BACK_TO_LOGIN": "Login via Password",
@@ -34,7 +34,7 @@
},
"SUBMIT": "Continue with SSO",
"API": {
"ERROR_MESSAGE": "SSO authentication failed"
"ERROR_MESSAGE": "SSO authentication failed. Please check your credentials and try again."
}
}
}

View File

@@ -27,15 +27,20 @@
"LABEL": "Heslo",
"PLACEHOLDER": "Heslo",
"ERROR": "Heslo je příliš krátké",
"IS_INVALID_PASSWORD": "Heslo by mělo obsahovat alespoň jedno velké písmeno, jedno malé písmeno, jedno číslo a jeden speciální znak"
"IS_INVALID_PASSWORD": "Heslo by mělo obsahovat alespoň jedno velké písmeno, jedno malé písmeno, jedno číslo a jeden speciální znak",
"REQUIREMENTS_LENGTH": "At least 6 characters long",
"REQUIREMENTS_UPPERCASE": "At least one uppercase letter",
"REQUIREMENTS_LOWERCASE": "At least one lowercase letter",
"REQUIREMENTS_NUMBER": "At least one number",
"REQUIREMENTS_SPECIAL": "At least one special character"
},
"CONFIRM_PASSWORD": {
"LABEL": "Potvrzení hesla",
"PLACEHOLDER": "Potvrzení hesla",
"ERROR": "Heslo se neshoduje"
"ERROR": "Hesla se neshodují."
},
"API": {
"SUCCESS_MESSAGE": "Registrace byla úspěšná",
"SUCCESS_MESSAGE": "Registration Successful",
"ERROR_MESSAGE": "Nelze se připojit k Woot serveru, opakujte akci později"
},
"SUBMIT": "Create account",

View File

@@ -227,6 +227,13 @@
"YES": "Send",
"CANCEL": "Annuller"
}
},
"QUOTED_REPLY": {
"ENABLE_TOOLTIP": "Include quoted email thread",
"DISABLE_TOOLTIP": "Don't include quoted email thread",
"REMOVE_PREVIEW": "Remove quoted email thread",
"COLLAPSE": "Collapse preview",
"EXPAND": "Expand preview"
}
},
"VISIBLE_TO_AGENTS": "Privat Note: Kun synlig for dig og dit team",

View File

@@ -5,6 +5,8 @@
"PLACEHOLDER": "Søg",
"EMPTY_STATE": "Ingen resultater fundet"
},
"CLOSE": "Luk"
"CLOSE": "Luk",
"BETA": "Beta",
"BETA_DESCRIPTION": "This feature is in beta and may change as we improve it."
}
}

View File

@@ -618,6 +618,11 @@
"SETTINGS_POPUP": {
"MESSENGER_HEADING": "Messenger- Script",
"MESSENGER_SUB_HEAD": "Placer denne knap inde i din body tag",
"ALLOWED_DOMAINS": {
"TITLE": "Allowed Domains",
"SUBTITLE": "Add wildcard or regular domains separated by commas (leave blank to allow all), e.g. *.chatwoot.dev, chatwoot.com.",
"PLACEHOLDER": "Enter domains separated by commas (eg: *.chatwoot.dev, chatwoot.com)"
},
"INBOX_AGENTS": "Agenter",
"INBOX_AGENTS_SUB_TEXT": "Tilføj eller fjern agenter fra denne indbakke",
"AGENT_ASSIGNMENT": "Samtale Tildeling",

View File

@@ -24,7 +24,7 @@
"CREATE_NEW_ACCOUNT": "Opret ny konto",
"SUBMIT": "Log Ind",
"SAML": {
"LABEL": "Log in via SSO",
"LABEL": "Login via SSO",
"TITLE": "Initiate Single Sign-on (SSO)",
"SUBTITLE": "Enter your work email to access your organization",
"BACK_TO_LOGIN": "Login via Password",
@@ -34,7 +34,7 @@
},
"SUBMIT": "Continue with SSO",
"API": {
"ERROR_MESSAGE": "SSO authentication failed"
"ERROR_MESSAGE": "SSO authentication failed. Please check your credentials and try again."
}
}
}

View File

@@ -27,15 +27,20 @@
"LABEL": "Adgangskode",
"PLACEHOLDER": "Adgangskode",
"ERROR": "Adgangskoden er for kort",
"IS_INVALID_PASSWORD": "Adgangskoden skal indeholde mindst 1 stort bogstav, 1 lille bogstav, 1 nummer og 1 specialtegn"
"IS_INVALID_PASSWORD": "Adgangskoden skal indeholde mindst 1 stort bogstav, 1 lille bogstav, 1 nummer og 1 specialtegn",
"REQUIREMENTS_LENGTH": "At least 6 characters long",
"REQUIREMENTS_UPPERCASE": "At least one uppercase letter",
"REQUIREMENTS_LOWERCASE": "At least one lowercase letter",
"REQUIREMENTS_NUMBER": "At least one number",
"REQUIREMENTS_SPECIAL": "At least one special character"
},
"CONFIRM_PASSWORD": {
"LABEL": "Bekræft Adgangskode",
"PLACEHOLDER": "Bekræft Adgangskode",
"ERROR": "Adgangskode stemmer ikke overens"
"ERROR": "Adgangskoder stemmer ikke overens."
},
"API": {
"SUCCESS_MESSAGE": "Registrering Succesfuld",
"SUCCESS_MESSAGE": "Registration Successful",
"ERROR_MESSAGE": "Kunne ikke oprette forbindelse til Woot Server, Prøv igen senere"
},
"SUBMIT": "Opret en konto",

View File

@@ -227,6 +227,13 @@
"YES": "Senden",
"CANCEL": "Abbrechen"
}
},
"QUOTED_REPLY": {
"ENABLE_TOOLTIP": "Include quoted email thread",
"DISABLE_TOOLTIP": "Don't include quoted email thread",
"REMOVE_PREVIEW": "Remove quoted email thread",
"COLLAPSE": "Collapse preview",
"EXPAND": "Expand preview"
}
},
"VISIBLE_TO_AGENTS": "Privater Hinweis: Nur für Sie und Ihr Team sichtbar",

View File

@@ -5,6 +5,8 @@
"PLACEHOLDER": "Suchen",
"EMPTY_STATE": "Keine Ergebnisse gefunden"
},
"CLOSE": "Schließen"
"CLOSE": "Schließen",
"BETA": "Beta",
"BETA_DESCRIPTION": "This feature is in beta and may change as we improve it."
}
}

View File

@@ -618,6 +618,11 @@
"SETTINGS_POPUP": {
"MESSENGER_HEADING": "Messenger-Skript",
"MESSENGER_SUB_HEAD": "Platzieren Sie diese Schaltfläche in Ihrem Body-Tag",
"ALLOWED_DOMAINS": {
"TITLE": "Allowed Domains",
"SUBTITLE": "Add wildcard or regular domains separated by commas (leave blank to allow all), e.g. *.chatwoot.dev, chatwoot.com.",
"PLACEHOLDER": "Enter domains separated by commas (eg: *.chatwoot.dev, chatwoot.com)"
},
"INBOX_AGENTS": "Agenten",
"INBOX_AGENTS_SUB_TEXT": "Hinzufügen oder Entfernen von Agenten zu diesem Posteingang",
"AGENT_ASSIGNMENT": "Konversationssauftrag",

View File

@@ -24,7 +24,7 @@
"CREATE_NEW_ACCOUNT": "Neuen Account erstellen",
"SUBMIT": "Einloggen",
"SAML": {
"LABEL": "Log in via SSO",
"LABEL": "Login via SSO",
"TITLE": "Initiate Single Sign-on (SSO)",
"SUBTITLE": "Enter your work email to access your organization",
"BACK_TO_LOGIN": "Login via Password",
@@ -34,7 +34,7 @@
},
"SUBMIT": "Continue with SSO",
"API": {
"ERROR_MESSAGE": "SSO authentication failed"
"ERROR_MESSAGE": "SSO authentication failed. Please check your credentials and try again."
}
}
}

View File

@@ -27,15 +27,20 @@
"LABEL": "Passwort",
"PLACEHOLDER": "Passwort",
"ERROR": "Das Passwort ist zu kurz",
"IS_INVALID_PASSWORD": "Das Passwort sollte mindestens 1 Großbuchstaben, 1 Kleinbuchstaben, 1 Ziffer und 1 Sonderzeichen enthalten"
"IS_INVALID_PASSWORD": "Das Passwort sollte mindestens 1 Großbuchstaben, 1 Kleinbuchstaben, 1 Ziffer und 1 Sonderzeichen enthalten",
"REQUIREMENTS_LENGTH": "At least 6 characters long",
"REQUIREMENTS_UPPERCASE": "At least one uppercase letter",
"REQUIREMENTS_LOWERCASE": "At least one lowercase letter",
"REQUIREMENTS_NUMBER": "At least one number",
"REQUIREMENTS_SPECIAL": "At least one special character"
},
"CONFIRM_PASSWORD": {
"LABEL": "Bestätige das Passwort",
"PLACEHOLDER": "Bestätige das Passwort",
"ERROR": "Passwort stimmt nicht überein"
"ERROR": "Passwörter stimmen nicht überein."
},
"API": {
"SUCCESS_MESSAGE": "Registrierung erfolgreich",
"SUCCESS_MESSAGE": "Registration Successful",
"ERROR_MESSAGE": "Es konnte keine Verbindung zum Woot Server hergestellt werden. Bitte versuchen Sie es später erneut"
},
"SUBMIT": "Konto erstellen",

View File

@@ -227,6 +227,13 @@
"YES": "Αποστολή",
"CANCEL": "Άκυρο"
}
},
"QUOTED_REPLY": {
"ENABLE_TOOLTIP": "Include quoted email thread",
"DISABLE_TOOLTIP": "Don't include quoted email thread",
"REMOVE_PREVIEW": "Remove quoted email thread",
"COLLAPSE": "Collapse preview",
"EXPAND": "Expand preview"
}
},
"VISIBLE_TO_AGENTS": "Ιδιωτική Σημείωση: Ορατή μόνο σε σας και την ομάδα σας",

View File

@@ -5,6 +5,8 @@
"PLACEHOLDER": "Αναζήτηση",
"EMPTY_STATE": "Δεν βρέθηκαν αποτελέσματα"
},
"CLOSE": "Κλείσιμο"
"CLOSE": "Κλείσιμο",
"BETA": "Beta",
"BETA_DESCRIPTION": "This feature is in beta and may change as we improve it."
}
}

View File

@@ -618,6 +618,11 @@
"SETTINGS_POPUP": {
"MESSENGER_HEADING": "Κώδικας (Script)",
"MESSENGER_SUB_HEAD": "Τοποθετήσετε αυτόν τον κώδικα μέσα στο body tag της ιστοσελίδας σας",
"ALLOWED_DOMAINS": {
"TITLE": "Allowed Domains",
"SUBTITLE": "Add wildcard or regular domains separated by commas (leave blank to allow all), e.g. *.chatwoot.dev, chatwoot.com.",
"PLACEHOLDER": "Enter domains separated by commas (eg: *.chatwoot.dev, chatwoot.com)"
},
"INBOX_AGENTS": "Πράκτορες",
"INBOX_AGENTS_SUB_TEXT": "Προσθέστε ή αφαιρέστε πράκτορες σε αυτό το κιβώτιο",
"AGENT_ASSIGNMENT": "Ανάθεση Συνομιλίας",

View File

@@ -24,7 +24,7 @@
"CREATE_NEW_ACCOUNT": "Δημιουργία νέου Λογαριασμού",
"SUBMIT": "Είσοδος",
"SAML": {
"LABEL": "Log in via SSO",
"LABEL": "Login via SSO",
"TITLE": "Initiate Single Sign-on (SSO)",
"SUBTITLE": "Enter your work email to access your organization",
"BACK_TO_LOGIN": "Login via Password",
@@ -34,7 +34,7 @@
},
"SUBMIT": "Continue with SSO",
"API": {
"ERROR_MESSAGE": "SSO authentication failed"
"ERROR_MESSAGE": "SSO authentication failed. Please check your credentials and try again."
}
}
}

View File

@@ -27,15 +27,20 @@
"LABEL": "Κωδικός",
"PLACEHOLDER": "Κωδικός",
"ERROR": "Ο κωδικός είναι πολύ σύντομος",
"IS_INVALID_PASSWORD": "Ο κωδικός πρόσβασης πρέπει να περιέχει τουλάχιστον 1 κεφαλαίο γράμμα, 1 πεζό γράμμα, 1 αριθμό και 1 ειδικό χαρακτήρα"
"IS_INVALID_PASSWORD": "Ο κωδικός πρόσβασης πρέπει να περιέχει τουλάχιστον 1 κεφαλαίο γράμμα, 1 πεζό γράμμα, 1 αριθμό και 1 ειδικό χαρακτήρα",
"REQUIREMENTS_LENGTH": "At least 6 characters long",
"REQUIREMENTS_UPPERCASE": "At least one uppercase letter",
"REQUIREMENTS_LOWERCASE": "At least one lowercase letter",
"REQUIREMENTS_NUMBER": "At least one number",
"REQUIREMENTS_SPECIAL": "At least one special character"
},
"CONFIRM_PASSWORD": {
"LABEL": "Επιβεβαίωση κωδικού",
"PLACEHOLDER": "Επιβεβαίωση κωδικού",
"ERROR": "Οι κωδικοί δεν συμφωνούν"
"ERROR": "Οι κωδικοί δεν ταιριάζουν."
},
"API": {
"SUCCESS_MESSAGE": "Επιτυχής καταχώρηση",
"SUCCESS_MESSAGE": "Registration Successful",
"ERROR_MESSAGE": "Αδυναμία σύνδεσης με τον Woot Server, Παρακαλώ προσπαθήστε αργότερα"
},
"SUBMIT": "Create account",

View File

@@ -227,6 +227,13 @@
"YES": "Send",
"CANCEL": "Cancel"
}
},
"QUOTED_REPLY": {
"ENABLE_TOOLTIP": "Include quoted email thread",
"DISABLE_TOOLTIP": "Don't include quoted email thread",
"REMOVE_PREVIEW": "Remove quoted email thread",
"COLLAPSE": "Collapse preview",
"EXPAND": "Expand preview"
}
},
"VISIBLE_TO_AGENTS": "Private Note: Only visible to you and your team",

View File

@@ -5,6 +5,8 @@
"LEARN_MORE": "Learn more about inboxes",
"RECONNECTION_REQUIRED": "Your inbox is disconnected. You won't receive new messages until you reauthorize it.",
"CLICK_TO_RECONNECT": "Click here to reconnect.",
"WHATSAPP_REGISTRATION_INCOMPLETE": "Your WhatsApp Business registration isnt complete. Please check your display name status in Meta Business Manager before reconnecting.",
"COMPLETE_REGISTRATION": "Complete Registration",
"LIST": {
"404": "There are no inboxes attached to this account."
},
@@ -605,8 +607,62 @@
"BUSINESS_HOURS": "Business Hours",
"WIDGET_BUILDER": "Widget Builder",
"BOT_CONFIGURATION": "Bot Configuration",
"ACCOUNT_HEALTH": "Account Health",
"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",
"FIELDS": {
"DISPLAY_PHONE_NUMBER": {
"LABEL": "Display phone number",
"TOOLTIP": "Phone number displayed to customers"
},
"VERIFIED_NAME": {
"LABEL": "Business name",
"TOOLTIP": "Business name verified by WhatsApp"
},
"DISPLAY_NAME_STATUS": {
"LABEL": "Display name status",
"TOOLTIP": "Status of your business name verification"
},
"QUALITY_RATING": {
"LABEL": "Quality rating",
"TOOLTIP": "WhatsApp quality rating for your account"
},
"MESSAGING_LIMIT_TIER": {
"LABEL": "Messaging limit tier",
"TOOLTIP": "Daily messaging limit for your account"
},
"ACCOUNT_MODE": {
"LABEL": "Account mode",
"TOOLTIP": "Current operating mode of your WhatsApp account"
}
},
"VALUES": {
"TIERS": {
"TIER_250": "250 customers per 24h",
"TIER_1000": "1K customers per 24h",
"TIER_1K": "1K customers per 24h",
"TIER_10K": "10K customers per 24h",
"TIER_100K": "100K customers per 24h",
"TIER_UNLIMITED": "Unlimited customers per 24h"
},
"STATUSES": {
"APPROVED": "Approved",
"PENDING_REVIEW": "Pending Review",
"AVAILABLE_WITHOUT_REVIEW": "Available Without Review",
"REJECTED": "Rejected",
"DECLINED": "Declined"
},
"MODES": {
"SANDBOX": "Sandbox",
"LIVE": "Live"
}
}
},
"SETTINGS": "Settings",
"FEATURES": {
"LABEL": "Features",

View File

@@ -24,7 +24,7 @@
"CREATE_NEW_ACCOUNT": "Create a new account",
"SUBMIT": "Login",
"SAML": {
"LABEL": "Log in via SSO",
"LABEL": "Login via SSO",
"TITLE": "Initiate Single Sign-on (SSO)",
"SUBTITLE": "Enter your work email to access your organization",
"BACK_TO_LOGIN": "Login via Password",

View File

@@ -27,15 +27,20 @@
"LABEL": "Password",
"PLACEHOLDER": "Password",
"ERROR": "Password is too short.",
"IS_INVALID_PASSWORD": "Password should contain atleast 1 uppercase letter, 1 lowercase letter, 1 number and 1 special character."
"IS_INVALID_PASSWORD": "Password should contain atleast 1 uppercase letter, 1 lowercase letter, 1 number and 1 special character.",
"REQUIREMENTS_LENGTH": "At least 6 characters long",
"REQUIREMENTS_UPPERCASE": "At least one uppercase letter",
"REQUIREMENTS_LOWERCASE": "At least one lowercase letter",
"REQUIREMENTS_NUMBER": "At least one number",
"REQUIREMENTS_SPECIAL": "At least one special character"
},
"CONFIRM_PASSWORD": {
"LABEL": "Confirm password",
"PLACEHOLDER": "Confirm password",
"ERROR": "Password doesnot match."
"ERROR": "Passwords do not match."
},
"API": {
"SUCCESS_MESSAGE": "Registration Successfull",
"SUCCESS_MESSAGE": "Registration Successful",
"ERROR_MESSAGE": "Could not connect to Woot server. Please try again."
},
"SUBMIT": "Create account",

View File

@@ -227,6 +227,13 @@
"YES": "Enviar",
"CANCEL": "Cancelar"
}
},
"QUOTED_REPLY": {
"ENABLE_TOOLTIP": "Include quoted email thread",
"DISABLE_TOOLTIP": "Don't include quoted email thread",
"REMOVE_PREVIEW": "Remove quoted email thread",
"COLLAPSE": "Collapse preview",
"EXPAND": "Expand preview"
}
},
"VISIBLE_TO_AGENTS": "Nota privada: solo visible para ti y tu equipo",

View File

@@ -5,6 +5,8 @@
"PLACEHOLDER": "Buscar",
"EMPTY_STATE": "No se encontraron resultados"
},
"CLOSE": "Cerrar"
"CLOSE": "Cerrar",
"BETA": "Beta",
"BETA_DESCRIPTION": "This feature is in beta and may change as we improve it."
}
}

View File

@@ -618,6 +618,11 @@
"SETTINGS_POPUP": {
"MESSENGER_HEADING": "Script de Messenger",
"MESSENGER_SUB_HEAD": "Coloca este botón dentro de tu etiqueta cuerpo",
"ALLOWED_DOMAINS": {
"TITLE": "Allowed Domains",
"SUBTITLE": "Add wildcard or regular domains separated by commas (leave blank to allow all), e.g. *.chatwoot.dev, chatwoot.com.",
"PLACEHOLDER": "Enter domains separated by commas (eg: *.chatwoot.dev, chatwoot.com)"
},
"INBOX_AGENTS": "Agentes",
"INBOX_AGENTS_SUB_TEXT": "Añadir o quitar agentes de esta bandeja de entrada",
"AGENT_ASSIGNMENT": "Asignación de conversación",

View File

@@ -24,7 +24,7 @@
"CREATE_NEW_ACCOUNT": "Crear nueva cuenta",
"SUBMIT": "Iniciar sesión",
"SAML": {
"LABEL": "Log in via SSO",
"LABEL": "Login via SSO",
"TITLE": "Initiate Single Sign-on (SSO)",
"SUBTITLE": "Enter your work email to access your organization",
"BACK_TO_LOGIN": "Login via Password",
@@ -34,7 +34,7 @@
},
"SUBMIT": "Continue with SSO",
"API": {
"ERROR_MESSAGE": "SSO authentication failed"
"ERROR_MESSAGE": "SSO authentication failed. Please check your credentials and try again."
}
}
}

View File

@@ -27,15 +27,20 @@
"LABEL": "Contraseña",
"PLACEHOLDER": "Contraseña",
"ERROR": "La contraseña es demasiado corta",
"IS_INVALID_PASSWORD": "La contraseña debe contener al menos 1 letra mayúscula, 1 letra minúscula, 1 número y 1 carácter especial"
"IS_INVALID_PASSWORD": "La contraseña debe contener al menos 1 letra mayúscula, 1 letra minúscula, 1 número y 1 carácter especial",
"REQUIREMENTS_LENGTH": "At least 6 characters long",
"REQUIREMENTS_UPPERCASE": "At least one uppercase letter",
"REQUIREMENTS_LOWERCASE": "At least one lowercase letter",
"REQUIREMENTS_NUMBER": "At least one number",
"REQUIREMENTS_SPECIAL": "At least one special character"
},
"CONFIRM_PASSWORD": {
"LABEL": "Confirmar contraseña",
"PLACEHOLDER": "Confirmar contraseña",
"ERROR": "La contraseña no coincide"
"ERROR": "Las contraseñas no coinciden."
},
"API": {
"SUCCESS_MESSAGE": "Registro Exitoso",
"SUCCESS_MESSAGE": "Registration Successful",
"ERROR_MESSAGE": "No se pudo conectar al servidor Woot, por favor inténtalo de nuevo más tarde"
},
"SUBMIT": "Crear una cuenta",

View File

@@ -227,6 +227,13 @@
"YES": "ارسال",
"CANCEL": "انصراف"
}
},
"QUOTED_REPLY": {
"ENABLE_TOOLTIP": "Include quoted email thread",
"DISABLE_TOOLTIP": "Don't include quoted email thread",
"REMOVE_PREVIEW": "Remove quoted email thread",
"COLLAPSE": "Collapse preview",
"EXPAND": "Expand preview"
}
},
"VISIBLE_TO_AGENTS": "یادداشت خصوصی: فقط برای شما و تیم شما قابل مشاهده است",

View File

@@ -5,6 +5,8 @@
"PLACEHOLDER": "جستجو",
"EMPTY_STATE": "نتیجه‌ای یافت نشد"
},
"CLOSE": "بستن"
"CLOSE": "بستن",
"BETA": "آزمایشی",
"BETA_DESCRIPTION": "This feature is in beta and may change as we improve it."
}
}

View File

@@ -618,6 +618,11 @@
"SETTINGS_POPUP": {
"MESSENGER_HEADING": "اسکریپت ویجت",
"MESSENGER_SUB_HEAD": "این دکمه را در تگ body قرار دهید",
"ALLOWED_DOMAINS": {
"TITLE": "Allowed Domains",
"SUBTITLE": "Add wildcard or regular domains separated by commas (leave blank to allow all), e.g. *.chatwoot.dev, chatwoot.com.",
"PLACEHOLDER": "Enter domains separated by commas (eg: *.chatwoot.dev, chatwoot.com)"
},
"INBOX_AGENTS": "ایجنت ها",
"INBOX_AGENTS_SUB_TEXT": "اضافه کردن یا حذف کردن دسترسی ایجنت به صندوق ورودی",
"AGENT_ASSIGNMENT": "اختصاص گفتگو",

View File

@@ -24,7 +24,7 @@
"CREATE_NEW_ACCOUNT": "حساب جدید بسازید",
"SUBMIT": "ورود",
"SAML": {
"LABEL": "Log in via SSO",
"LABEL": "Login via SSO",
"TITLE": "Initiate Single Sign-on (SSO)",
"SUBTITLE": "Enter your work email to access your organization",
"BACK_TO_LOGIN": "Login via Password",
@@ -34,7 +34,7 @@
},
"SUBMIT": "Continue with SSO",
"API": {
"ERROR_MESSAGE": "SSO authentication failed"
"ERROR_MESSAGE": "SSO authentication failed. Please check your credentials and try again."
}
}
}

View File

@@ -27,15 +27,20 @@
"LABEL": "رمز عبور",
"PLACEHOLDER": "رمز عبور",
"ERROR": "رمز عبور خیلی کوتاه است",
"IS_INVALID_PASSWORD": "رمز عبور باید شامل حداقل ۱ حرف بزرگ، ۱ حرف کوچک، ۱ عدد و ۱ کاراکتر خاص باشد"
"IS_INVALID_PASSWORD": "رمز عبور باید شامل حداقل ۱ حرف بزرگ، ۱ حرف کوچک، ۱ عدد و ۱ کاراکتر خاص باشد",
"REQUIREMENTS_LENGTH": "At least 6 characters long",
"REQUIREMENTS_UPPERCASE": "At least one uppercase letter",
"REQUIREMENTS_LOWERCASE": "At least one lowercase letter",
"REQUIREMENTS_NUMBER": "At least one number",
"REQUIREMENTS_SPECIAL": "At least one special character"
},
"CONFIRM_PASSWORD": {
"LABEL": "تکرار رمز عبور",
"PLACEHOLDER": "تکرار رمز عبور",
"ERROR": "رمز عبور و تکرار رمز عبور یکسان نیستند"
"ERROR": "تکرار رمز عبور می‌بایست با رمز عبور یکسان باشد."
},
"API": {
"SUCCESS_MESSAGE": "ثبت نام با موفقیت انجام شد",
"SUCCESS_MESSAGE": "Registration Successful",
"ERROR_MESSAGE": "ارتباط با سرور برقرار نشد، لطفا بعدا امتحان کنید"
},
"SUBMIT": "ایجاد حساب کاربری",

View File

@@ -227,6 +227,13 @@
"YES": "Lähetä",
"CANCEL": "Peruuta"
}
},
"QUOTED_REPLY": {
"ENABLE_TOOLTIP": "Include quoted email thread",
"DISABLE_TOOLTIP": "Don't include quoted email thread",
"REMOVE_PREVIEW": "Remove quoted email thread",
"COLLAPSE": "Collapse preview",
"EXPAND": "Expand preview"
}
},
"VISIBLE_TO_AGENTS": "Yksityinen huomautus: Näkyy vain sinulle ja tiimillesi",

View File

@@ -5,6 +5,8 @@
"PLACEHOLDER": "Etsi",
"EMPTY_STATE": "Tuloksia ei löytynyt"
},
"CLOSE": "Sulje"
"CLOSE": "Sulje",
"BETA": "Beta",
"BETA_DESCRIPTION": "This feature is in beta and may change as we improve it."
}
}

View File

@@ -618,6 +618,11 @@
"SETTINGS_POPUP": {
"MESSENGER_HEADING": "Messenger-skripti",
"MESSENGER_SUB_HEAD": "Aseta tämä painike body-tagiisi",
"ALLOWED_DOMAINS": {
"TITLE": "Allowed Domains",
"SUBTITLE": "Add wildcard or regular domains separated by commas (leave blank to allow all), e.g. *.chatwoot.dev, chatwoot.com.",
"PLACEHOLDER": "Enter domains separated by commas (eg: *.chatwoot.dev, chatwoot.com)"
},
"INBOX_AGENTS": "Edustajat",
"INBOX_AGENTS_SUB_TEXT": "Lisää tai poista edustajia tästä saapuneet-kansiosta",
"AGENT_ASSIGNMENT": "Conversation Assignment",

View File

@@ -24,7 +24,7 @@
"CREATE_NEW_ACCOUNT": "Luo uusi tili",
"SUBMIT": "Kirjaudu",
"SAML": {
"LABEL": "Log in via SSO",
"LABEL": "Login via SSO",
"TITLE": "Initiate Single Sign-on (SSO)",
"SUBTITLE": "Enter your work email to access your organization",
"BACK_TO_LOGIN": "Login via Password",
@@ -34,7 +34,7 @@
},
"SUBMIT": "Continue with SSO",
"API": {
"ERROR_MESSAGE": "SSO authentication failed"
"ERROR_MESSAGE": "SSO authentication failed. Please check your credentials and try again."
}
}
}

View File

@@ -27,15 +27,20 @@
"LABEL": "Salasana",
"PLACEHOLDER": "Salasana",
"ERROR": "Salasana on liian lyhyt",
"IS_INVALID_PASSWORD": "Salasanan tulee sisältää vähintään 1 iso kirjain, 1 pieni kirjain, 1 numero ja 1 erikoismerkki."
"IS_INVALID_PASSWORD": "Salasanan tulee sisältää vähintään 1 iso kirjain, 1 pieni kirjain, 1 numero ja 1 erikoismerkki.",
"REQUIREMENTS_LENGTH": "At least 6 characters long",
"REQUIREMENTS_UPPERCASE": "At least one uppercase letter",
"REQUIREMENTS_LOWERCASE": "At least one lowercase letter",
"REQUIREMENTS_NUMBER": "At least one number",
"REQUIREMENTS_SPECIAL": "At least one special character"
},
"CONFIRM_PASSWORD": {
"LABEL": "Vahvista salasana",
"PLACEHOLDER": "Vahvista salasana",
"ERROR": "Salasanat eivät täsmää"
"ERROR": "Salasanat eivät täsmää."
},
"API": {
"SUCCESS_MESSAGE": "Rekisteröinti onnistui",
"SUCCESS_MESSAGE": "Registration Successful",
"ERROR_MESSAGE": "Yhteyden muodostaminen Woot-palvelimelle ei onnistunut, yritä myöhemmin uudelleen"
},
"SUBMIT": "Create account",

View File

@@ -227,6 +227,13 @@
"YES": "Envoyer",
"CANCEL": "Annuler"
}
},
"QUOTED_REPLY": {
"ENABLE_TOOLTIP": "Include quoted email thread",
"DISABLE_TOOLTIP": "Don't include quoted email thread",
"REMOVE_PREVIEW": "Remove quoted email thread",
"COLLAPSE": "Collapse preview",
"EXPAND": "Expand preview"
}
},
"VISIBLE_TO_AGENTS": "Note privée : uniquement visible par vous et votre équipe",

View File

@@ -5,6 +5,8 @@
"PLACEHOLDER": "Rechercher",
"EMPTY_STATE": "Aucun résultat trouvé"
},
"CLOSE": "Fermer"
"CLOSE": "Fermer",
"BETA": "Bêta",
"BETA_DESCRIPTION": "This feature is in beta and may change as we improve it."
}
}

View File

@@ -618,6 +618,11 @@
"SETTINGS_POPUP": {
"MESSENGER_HEADING": "Script du Widget Web",
"MESSENGER_SUB_HEAD": "Placez ce code avant la fermeture de votre balise body",
"ALLOWED_DOMAINS": {
"TITLE": "Allowed Domains",
"SUBTITLE": "Add wildcard or regular domains separated by commas (leave blank to allow all), e.g. *.chatwoot.dev, chatwoot.com.",
"PLACEHOLDER": "Enter domains separated by commas (eg: *.chatwoot.dev, chatwoot.com)"
},
"INBOX_AGENTS": "Agents",
"INBOX_AGENTS_SUB_TEXT": "Ajouter ou supprimer des agents de cette boîte de réception",
"AGENT_ASSIGNMENT": "Konversationsauftrag",

View File

@@ -24,7 +24,7 @@
"CREATE_NEW_ACCOUNT": "Créer un nouveau compte",
"SUBMIT": "Se connecter",
"SAML": {
"LABEL": "Log in via SSO",
"LABEL": "Login via SSO",
"TITLE": "Initiate Single Sign-on (SSO)",
"SUBTITLE": "Enter your work email to access your organization",
"BACK_TO_LOGIN": "Login via Password",
@@ -34,7 +34,7 @@
},
"SUBMIT": "Continue with SSO",
"API": {
"ERROR_MESSAGE": "SSO authentication failed"
"ERROR_MESSAGE": "SSO authentication failed. Please check your credentials and try again."
}
}
}

View File

@@ -27,15 +27,20 @@
"LABEL": "Mot de passe",
"PLACEHOLDER": "Mot de passe",
"ERROR": "Le mot de passe est trop court",
"IS_INVALID_PASSWORD": "Le mot de passe doit contenir au moins 1 lettre majuscule, 1 lettre minuscule, 1 chiffre et 1 caractère spécial"
"IS_INVALID_PASSWORD": "Le mot de passe doit contenir au moins 1 lettre majuscule, 1 lettre minuscule, 1 chiffre et 1 caractère spécial",
"REQUIREMENTS_LENGTH": "At least 6 characters long",
"REQUIREMENTS_UPPERCASE": "At least one uppercase letter",
"REQUIREMENTS_LOWERCASE": "At least one lowercase letter",
"REQUIREMENTS_NUMBER": "At least one number",
"REQUIREMENTS_SPECIAL": "At least one special character"
},
"CONFIRM_PASSWORD": {
"LABEL": "Confirmer le mot de passe",
"PLACEHOLDER": "Confirmer le mot de passe",
"ERROR": "Les mots de passe ne correspondent pas"
"ERROR": "Les mots de passe ne correspondent pas."
},
"API": {
"SUCCESS_MESSAGE": "Inscription réussie",
"SUCCESS_MESSAGE": "Registration Successful",
"ERROR_MESSAGE": "Impossible de se connecter au serveur Woot, veuillez réessayer plus tard"
},
"SUBMIT": "Créer un compte",

View File

@@ -227,6 +227,13 @@
"YES": "שלח",
"CANCEL": "ביטול"
}
},
"QUOTED_REPLY": {
"ENABLE_TOOLTIP": "Include quoted email thread",
"DISABLE_TOOLTIP": "Don't include quoted email thread",
"REMOVE_PREVIEW": "Remove quoted email thread",
"COLLAPSE": "Collapse preview",
"EXPAND": "Expand preview"
}
},
"VISIBLE_TO_AGENTS": "פתקים פרטיים: רק אתה והצוות שלך יכולים לראות",

View File

@@ -5,6 +5,8 @@
"PLACEHOLDER": "חפש",
"EMPTY_STATE": "לא נמצאו תוצאות"
},
"CLOSE": "סגור"
"CLOSE": "סגור",
"BETA": "בטא",
"BETA_DESCRIPTION": "This feature is in beta and may change as we improve it."
}
}

View File

@@ -618,6 +618,11 @@
"SETTINGS_POPUP": {
"MESSENGER_HEADING": "סקריפט מסנג'ר",
"MESSENGER_SUB_HEAD": "מקם את הכפתור הזה בתוך תג הגוף שלך",
"ALLOWED_DOMAINS": {
"TITLE": "Allowed Domains",
"SUBTITLE": "Add wildcard or regular domains separated by commas (leave blank to allow all), e.g. *.chatwoot.dev, chatwoot.com.",
"PLACEHOLDER": "Enter domains separated by commas (eg: *.chatwoot.dev, chatwoot.com)"
},
"INBOX_AGENTS": "סוכנים",
"INBOX_AGENTS_SUB_TEXT": "הוסף או הסר נציגים מתיבת הדואר הנכנס הזו",
"AGENT_ASSIGNMENT": "שיוך שיחה",

View File

@@ -24,7 +24,7 @@
"CREATE_NEW_ACCOUNT": "צור חשבון",
"SUBMIT": "התחבר",
"SAML": {
"LABEL": "Log in via SSO",
"LABEL": "Login via SSO",
"TITLE": "Initiate Single Sign-on (SSO)",
"SUBTITLE": "Enter your work email to access your organization",
"BACK_TO_LOGIN": "Login via Password",
@@ -34,7 +34,7 @@
},
"SUBMIT": "Continue with SSO",
"API": {
"ERROR_MESSAGE": "SSO authentication failed"
"ERROR_MESSAGE": "SSO authentication failed. Please check your credentials and try again."
}
}
}

View File

@@ -27,15 +27,20 @@
"LABEL": "סיסמה",
"PLACEHOLDER": "סיסמה",
"ERROR": "הסיסמה קצרה מדי",
"IS_INVALID_PASSWORD": "הסיסמה צריכה להכיל לפחות אות אחת גדולה, אות קטנה אחת, מספר אחד ותו מיוחד אחד"
"IS_INVALID_PASSWORD": "הסיסמה צריכה להכיל לפחות אות אחת גדולה, אות קטנה אחת, מספר אחד ותו מיוחד אחד",
"REQUIREMENTS_LENGTH": "At least 6 characters long",
"REQUIREMENTS_UPPERCASE": "At least one uppercase letter",
"REQUIREMENTS_LOWERCASE": "At least one lowercase letter",
"REQUIREMENTS_NUMBER": "At least one number",
"REQUIREMENTS_SPECIAL": "At least one special character"
},
"CONFIRM_PASSWORD": {
"LABEL": "אמת סיסמה",
"PLACEHOLDER": "אמת סיסמה",
"ERROR": "סיסמה לא מתאימה"
"ERROR": "סיסמאות לא תואמות"
},
"API": {
"SUCCESS_MESSAGE": "ההרשמה הצליחה",
"SUCCESS_MESSAGE": "Registration Successful",
"ERROR_MESSAGE": "לא ניתן להתחבר לשרת Woot, נסה שוב מאוחר יותר"
},
"SUBMIT": "צור חשבון",

View File

@@ -227,6 +227,13 @@
"YES": "Send",
"CANCEL": "Cancel"
}
},
"QUOTED_REPLY": {
"ENABLE_TOOLTIP": "Include quoted email thread",
"DISABLE_TOOLTIP": "Don't include quoted email thread",
"REMOVE_PREVIEW": "Remove quoted email thread",
"COLLAPSE": "Collapse preview",
"EXPAND": "Expand preview"
}
},
"VISIBLE_TO_AGENTS": "Private Note: Only visible to you and your team",

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