Compare commits

..
Author SHA1 Message Date
Shivam Mishra ce276f888a fix: remove error loader 2024-10-04 18:09:28 +05:30
900 changed files with 7351 additions and 6378 deletions
+129 -109
View File
@@ -1,87 +1,84 @@
version: 2.1
orbs:
node: circleci/node@6.1.0
# Ruby CircleCI 2.0 configuration file
#
# Check https://circleci.com/docs/2.0/language-ruby/ for more details
#
version: 2
defaults: &defaults
working_directory: ~/build
machine:
image: ubuntu-2204:2024.05.1
resource_class: large
docker:
# specify the version you desire here
- image: cimg/ruby:3.3.3-browsers
# Specify service dependencies here if necessary
# CircleCI maintains a library of pre-built images
# documented at https://circleci.com/docs/2.0/circleci-images/
- image: cimg/postgres:15.3
- image: cimg/redis:6.2.6
environment:
RAILS_LOG_TO_STDOUT: false
COVERAGE: true
LOG_LEVEL: warn
- RAILS_LOG_TO_STDOUT: false
- COVERAGE: true
- LOG_LEVEL: warn
parallelism: 4
resource_class: large
jobs:
build:
<<: *defaults
steps:
- checkout
- node/install:
node-version: '20.12'
- node/install-pnpm
- node/install-packages:
pkg-manager: pnpm
override-ci-command: pnpm i
- run: node --version
- run: pnpm --version
- run:
name: Install System Dependencies
name: Configure Bundler
command: |
sudo apt-get update
DEBIAN_FRONTEND=noninteractive sudo apt-get install -y \
libpq-dev \
redis-server \
postgresql \
build-essential \
git \
curl \
libssl-dev \
zlib1g-dev \
libreadline-dev \
libyaml-dev \
openjdk-11-jdk \
jq \
software-properties-common \
ca-certificates \
imagemagick \
libxml2-dev \
libxslt1-dev \
file \
g++ \
gcc \
autoconf \
gnupg2 \
patch \
ruby-dev \
liblzma-dev \
libgmp-dev \
libncurses5-dev \
libffi-dev \
libgdbm6 \
libgdbm-dev \
libvips
- run:
name: Install RVM and Ruby 3.3.3
command: |
sudo apt-get install -y gpg
gpg --keyserver hkp://keyserver.ubuntu.com --recv-keys 409B6B1796C275462A1703113804BB82D39DC0E3 7D2BAF1CF37B13E2069D6956105BD0E739499BDB
\curl -sSL https://get.rvm.io | bash -s stable
echo 'source ~/.rvm/scripts/rvm' >> $BASH_ENV
source ~/.rvm/scripts/rvm
rvm install "3.3.3"
rvm use 3.3.3 --default
echo 'export BUNDLER_VERSION=$(cat Gemfile.lock | tail -1 | tr -d " ")' >> $BASH_ENV
source $BASH_ENV
gem install bundler
- run:
name: Install Application Dependencies
name: Which bundler?
command: bundle -v
- run:
name: Swap node versions
command: |
source ~/.rvm/scripts/rvm
bundle install
# pnpm install
set +e
wget -qO- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.1/install.sh | bash
export NVM_DIR="$HOME/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
[ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion"
nvm install v20
echo 'export NVM_DIR="$HOME/.nvm"' >> $BASH_ENV
echo '[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"' >> $BASH_ENV
# Run bundler
# Load installed gems from cache if possible, bundle install then save cache
# Multiple caches are used to increase the chance of a cache hit
- restore_cache:
keys:
- chatwoot-bundle-{{ .Environment.CACHE_VERSION }}-v20220524-{{ checksum "Gemfile.lock" }}
- run: bundle install --frozen --path ~/.bundle
- save_cache:
paths:
- ~/.bundle
key: chatwoot-bundle-{{ .Environment.CACHE_VERSION }}-v20220524-{{ checksum "Gemfile.lock" }}
# Only necessary if app uses webpacker or yarn in some other way
- restore_cache:
keys:
- chatwoot-yarn-{{ .Environment.CACHE_VERSION }}-{{ checksum "yarn.lock" }}
- chatwoot-yarn-
- run:
name: yarn
command: yarn install --frozen-lockfile --cache-folder ~/.cache/yarn
# Store yarn / webpacker cache
- save_cache:
key: chatwoot-yarn-{{ .Environment.CACHE_VERSION }}-{{ checksum "yarn.lock" }}
paths:
- ~/.cache/yarn
- run:
name: Download cc-test-reporter
@@ -89,8 +86,12 @@ jobs:
mkdir -p ~/tmp
curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ~/tmp/cc-test-reporter
chmod +x ~/tmp/cc-test-reporter
- persist_to_workspace:
root: ~/tmp
paths:
- cc-test-reporter
# Swagger verification
# verify swagger specification
- run:
name: Verify swagger API specification
command: |
@@ -103,62 +104,45 @@ jobs:
curl -L https://repo1.maven.org/maven2/org/openapitools/openapi-generator-cli/6.3.0/openapi-generator-cli-6.3.0.jar > ~/tmp/openapi-generator-cli-6.3.0.jar
java -jar ~/tmp/openapi-generator-cli-6.3.0.jar validate -i swagger/swagger.json
# we remove the FRONTED_URL from the .env before running the tests
- run:
name: Database Setup and Configure Environment Variables
command: |
pg_pass=$(head /dev/urandom | tr -dc A-Za-z0-9 | head -c 15 ; echo '')
sed -i "s/REPLACE_WITH_PASSWORD/${pg_pass}/g" ${PWD}/.circleci/setup_chatwoot.sql
chmod 644 ${PWD}/.circleci/setup_chatwoot.sql
mv ${PWD}/.circleci/setup_chatwoot.sql /tmp/
sudo -i -u postgres psql -f /tmp/setup_chatwoot.sql
cp .env.example .env
sed -i '/^FRONTEND_URL/d' .env
sed -i -e '/REDIS_URL/ s/=.*/=redis:\/\/localhost:6379/' .env
sed -i -e '/POSTGRES_HOST/ s/=.*/=localhost/' .env
sed -i -e '/POSTGRES_USERNAME/ s/=.*/=chatwoot/' .env
sed -i -e "/POSTGRES_PASSWORD/ s/=.*/=$pg_pass/" .env
echo -en "\nINSTALLATION_ENV=circleci" >> ".env"
# Database setup
- run:
name: Run DB migrations
command: bundle exec rails db:chatwoot_prepare
- run: bundle exec rake db:create
- run: bundle exec rake db:schema:load
# Bundle audit
- run:
name: Bundle audit
command: bundle exec bundle audit update && bundle exec bundle audit check -v
# Rubocop linting
- run:
name: Rubocop
command: bundle exec rubocop
# ESLint linting
# - run:
# name: Brakeman
# command: bundle exec brakeman
- run:
name: eslint
command: pnpm run eslint
command: yarn run eslint
# Run frontend tests
- run:
name: Run frontend tests
command: |
mkdir -p ~/build/coverage/frontend
mkdir -p ~/tmp/test-results/frontend_specs
~/tmp/cc-test-reporter before-build
pnpm run test:coverage
yarn test:coverage
- run:
name: Code Climate Test Coverage (Frontend)
name: Code Climate Test Coverage
command: |
~/tmp/cc-test-reporter format-coverage -t lcov -o "~/build/coverage/frontend/codeclimate.frontend_$CIRCLE_NODE_INDEX.json"
~/tmp/cc-test-reporter format-coverage -t lcov -o "coverage/codeclimate.frontend_$CIRCLE_NODE_INDEX.json"
# Run backend tests
# Run rails tests
- run:
name: Run backend tests
command: |
mkdir -p ~/tmp/test-results/rspec
mkdir -p ~/tmp/test-artifacts
mkdir -p ~/build/coverage/backend
mkdir -p coverage
~/tmp/cc-test-reporter before-build
TESTFILES=$(circleci tests glob "spec/**/*_spec.rb" | circleci tests split --split-by=timings)
bundle exec rspec --format progress \
@@ -166,18 +150,54 @@ jobs:
--out ~/tmp/test-results/rspec.xml \
-- ${TESTFILES}
no_output_timeout: 30m
- run:
name: Code Climate Test Coverage (Backend)
name: Code Climate Test Coverage
command: |
~/tmp/cc-test-reporter format-coverage -t simplecov -o "~/build/coverage/backend/codeclimate.$CIRCLE_NODE_INDEX.json"
- run:
name: List coverage directory contents
command: |
ls -R ~/build/coverage
~/tmp/cc-test-reporter format-coverage -t simplecov -o "coverage/codeclimate.$CIRCLE_NODE_INDEX.json"
- persist_to_workspace:
root: ~/build
root: coverage
paths:
- coverage
- codeclimate.*.json
# collect reports
- store_test_results:
path: ~/tmp/test-results
- store_artifacts:
path: ~/tmp/test-artifacts
- store_artifacts:
path: log
upload-coverage:
working_directory: ~/build
docker:
# specify the version you desire here
- image: circleci/ruby:3.0.2-node-browsers
environment:
- CC_TEST_REPORTER_ID: caf26a895e937974a90860cfadfded20891cfd1373a5aaafb3f67406ab9d433f
steps:
- attach_workspace:
at: ~/build
- run:
name: Download cc-test-reporter
command: |
mkdir -p ~/tmp
curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ~/tmp/cc-test-reporter
chmod +x ~/tmp/cc-test-reporter
- persist_to_workspace:
root: ~/tmp
paths:
- cc-test-reporter
- run:
name: Upload coverage results to Code Climate
command: |
~/tmp/cc-test-reporter sum-coverage --output - codeclimate.*.json | ~/tmp/cc-test-reporter upload-coverage --debug --input -
workflows:
version: 2
commit:
jobs:
- build
- upload-coverage:
requires:
- build
-11
View File
@@ -1,11 +0,0 @@
CREATE USER chatwoot CREATEDB;
ALTER USER chatwoot PASSWORD 'REPLACE_WITH_PASSWORD';
ALTER ROLE chatwoot SUPERUSER;
UPDATE pg_database SET datistemplate = FALSE WHERE datname = 'template1';
DROP DATABASE template1;
CREATE DATABASE template1 WITH TEMPLATE = template0 ENCODING = 'UNICODE';
UPDATE pg_database SET datistemplate = TRUE WHERE datname = 'template1';
\c template1;
VACUUM FREEZE;
+1 -2
View File
@@ -27,8 +27,7 @@ checks:
threshold: 50
exclude_patterns:
- 'spec/'
- '**/specs/**/**'
- '**/spec/**/**'
- '**/specs/'
- 'db/*'
- 'bin/**/*'
- 'db/**/*'
+1 -24
View File
@@ -1,23 +1,6 @@
module.exports = {
extends: [
'airbnb-base/legacy',
'prettier',
'plugin:vue/vue3-recommended',
'plugin:vitest-globals/recommended',
],
overrides: [
{
files: ['**/*.spec.{j,t}s?(x)'],
env: {
'vitest-globals/env': true,
},
},
],
extends: ['airbnb-base/legacy', 'prettier', 'plugin:vue/vue3-recommended'],
plugins: ['html', 'prettier'],
parserOptions: {
ecmaVersion: 'latest',
sourceType: 'module',
},
rules: {
'prettier/prettier': ['error'],
camelcase: 'off',
@@ -223,11 +206,5 @@ module.exports = {
globals: {
bus: true,
vi: true,
// beforeEach: true,
// afterEach: true,
// test: true,
// describe: true,
// it: true,
// expect: true,
},
};
-43
View File
@@ -1,43 +0,0 @@
name: Frontend Lint & Test
on:
push:
branches:
- develop
pull_request:
branches:
- develop
jobs:
test:
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.ref }}
repository: ${{ github.event.pull_request.head.repo.full_name }}
- uses: ruby/setup-ruby@v1
with:
bundler-cache: true
- uses: pnpm/action-setup@v4
with:
version: 9.3.0
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'pnpm'
- name: Install pnpm dependencies
run: pnpm install --frozen-lockfile
- name: Run eslint
run: pnpm run eslint
- name: Run frontend tests with coverage
run: |
mkdir -p coverage
pnpm run test:coverage
+36 -39
View File
@@ -21,7 +21,7 @@ jobs:
image: postgres:15.3
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: ''
POSTGRES_PASSWORD: ""
POSTGRES_DB: postgres
POSTGRES_HOST_AUTH_METHOD: trust
ports:
@@ -41,49 +41,46 @@ jobs:
options: --entrypoint redis-server
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
version: 9
ref: ${{ github.event.pull_request.head.ref }}
repository: ${{ github.event.pull_request.head.repo.full_name }}
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
version: 9
ref: ${{ github.event.pull_request.head.ref }}
repository: ${{ github.event.pull_request.head.repo.full_name }}
- uses: ruby/setup-ruby@v1
with:
bundler-cache: true # runs 'bundle install' and caches installed gems automatically
- uses: ruby/setup-ruby@v1
with:
bundler-cache: true # runs 'bundle install' and caches installed gems automatically
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'pnpm'
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'pnpm'
- name: Install pnpm dependencies
run: pnpm i
- name: Install pnpm dependencies
run: pnpm i
- name: Strip enterprise code
run: |
rm -rf enterprise
rm -rf spec/enterprise
- name: Strip enterprise code
run: |
rm -rf enterprise
rm -rf spec/enterprise
- name: Create database
run: bundle exec rake db:create
- name: Create database
run: bundle exec rake db:create
- name: Seed database
run: bundle exec rake db:schema:load
- name: Seed database
run: bundle exec rake db:schema:load
- name: Run frontend tests
run: pnpm run test:coverage
# Run rails tests
- name: Run backend tests
run: |
bundle exec rspec --profile=10 --format documentation
env:
NODE_OPTIONS: --openssl-legacy-provider
# Run rails tests
- name: Run backend tests
run: |
bundle exec rspec --profile=10 --format documentation
env:
NODE_OPTIONS: --openssl-legacy-provider
- name: Upload rails log folder
uses: actions/upload-artifact@v4
if: always()
with:
name: rails-log-folder
path: log
- name: Upload rails log folder
uses: actions/upload-artifact@v4
if: always()
with:
name: rails-log-folder
path: log
@@ -1,15 +1,6 @@
class Google::CallbacksController < OauthCallbackController
include GoogleConcern
def find_channel_by_email
# find by imap_login first, and then by email
# this ensures the legacy users can migrate correctly even if inbox email address doesn't match
imap_channel = Channel::Email.find_by(imap_login: users_data['email'], account: account)
return imap_channel if imap_channel
Channel::Email.find_by(email: users_data['email'], account: account)
end
private
def provider_name
+1 -5
View File
@@ -25,7 +25,7 @@ class OauthCallbackController < ApplicationController
end
def find_or_create_inbox
channel_email = find_channel_by_email
channel_email = Channel::Email.find_by(email: users_data['email'], account: account)
# we need this value to know where to redirect on sucessful processing of the callback
channel_exists = channel_email.present?
@@ -39,10 +39,6 @@ class OauthCallbackController < ApplicationController
[channel_email.inbox, channel_exists]
end
def find_channel_by_email
Channel::Email.find_by(email: users_data['email'], account: account)
end
def update_channel(channel_email)
channel_email.update!({
imap_login: users_data['email'], imap_address: imap_address,
+6 -10
View File
@@ -13,7 +13,6 @@ import { useStore } from 'dashboard/composables/store';
import WootSnackbarBox from './components/SnackbarContainer.vue';
import { setColorTheme } from './helper/themeHelper';
import { isOnOnboardingView } from 'v3/helpers/RouteHelper';
import { useAccount } from 'dashboard/composables/useAccount';
import {
registerSubscription,
verifyServiceWorkerExistence,
@@ -36,9 +35,8 @@ export default {
setup() {
const router = useRouter();
const store = useStore();
const { accountId } = useAccount();
return { router, store, currentAccountId: accountId };
return { router, store };
},
data() {
return {
@@ -54,6 +52,7 @@ export default {
currentUser: 'getCurrentUser',
authUIFlags: 'getAuthUIFlags',
accountUIFlags: 'accounts/getUIFlags',
currentAccountId: 'getCurrentAccountId',
}),
hasAccounts() {
const { accounts = [] } = this.currentUser || {};
@@ -70,13 +69,10 @@ export default {
this.showAddAccountModal = true;
}
},
currentAccountId: {
immediate: true,
handler() {
if (this.currentAccountId) {
this.initializeAccount();
}
},
currentAccountId() {
if (this.currentAccountId) {
this.initializeAccount();
}
},
},
mounted() {
@@ -27,14 +27,14 @@ export default {
<div class="flex flex-col items-start px-8 pt-8 pb-0">
<img v-if="headerImage" :src="headerImage" alt="No image" />
<h2
data-test-id="modal-header-title"
ref="modalHeaderTitle"
class="text-base font-semibold leading-6 text-slate-800 dark:text-slate-50"
>
{{ headerTitle }}
</h2>
<p
v-if="headerContent"
data-test-id="modal-header-content"
ref="modalHeaderContent"
class="w-full mt-2 text-sm leading-5 break-words text-slate-600 dark:text-slate-300"
>
{{ headerContent }}
@@ -105,7 +105,7 @@ export default {
size="small"
:color-scheme="status.disabled ? '' : 'secondary'"
:variant="status.disabled ? 'smooth' : 'clear'"
class="status-change--dropdown-button"
class-names="status-change--dropdown-button"
@click="changeAvailabilityStatus(status.value)"
>
<AvailabilityStatusBadge :status="status.value" />
@@ -2,7 +2,6 @@
import { mapGetters } from 'vuex';
import { getSidebarItems } from './config/default-sidebar';
import { useKeyboardEvents } from 'dashboard/composables/useKeyboardEvents';
import { useAccount } from 'dashboard/composables/useAccount';
import { useRoute, useRouter } from 'vue-router';
import PrimarySidebar from './sidebarComponents/Primary.vue';
@@ -34,7 +33,6 @@ export default {
setup(props, { emit }) {
const route = useRoute();
const router = useRouter();
const { accountId } = useAccount();
const toggleKeyShortcutModal = () => {
emit('openKeyShortcutModal');
@@ -74,7 +72,6 @@ export default {
return {
toggleKeyShortcutModal,
accountId,
};
},
data() {
@@ -85,6 +82,7 @@ export default {
computed: {
...mapGetters({
accountId: 'getCurrentAccountId',
currentUser: 'getCurrentUser',
globalConfig: 'globalConfig/get',
inboxes: 'inboxes/getInboxes',
@@ -1,62 +1,79 @@
import { mount } from '@vue/test-utils';
import { createStore } from 'vuex';
import AccountSelector from '../AccountSelector.vue';
import { createLocalVue, mount } from '@vue/test-utils';
import Vuex from 'vuex';
import VueI18n from 'vue-i18n';
import i18n from 'dashboard/i18n';
import WootModal from 'dashboard/components/Modal.vue';
import WootModalHeader from 'dashboard/components/ModalHeader.vue';
import FluentIcon from 'shared/components/FluentIcon/DashboardIcon.vue';
const store = createStore({
modules: {
auth: {
namespaced: false,
getters: {
getCurrentAccountId: () => 1,
getCurrentUser: () => ({
accounts: [
{ id: 1, name: 'Chatwoot', role: 'administrator' },
{ id: 2, name: 'GitX', role: 'agent' },
],
}),
},
},
globalConfig: {
namespaced: true,
getters: {
get: () => ({ createNewAccountFromDashboard: false }),
},
},
},
const localVue = createLocalVue();
localVue.component('woot-modal', WootModal);
localVue.component('woot-modal-header', WootModalHeader);
localVue.component('fluent-icon', FluentIcon);
localVue.use(Vuex);
localVue.use(VueI18n);
const i18nConfig = new VueI18n({
locale: 'en',
messages: i18n,
});
describe('AccountSelector', () => {
describe('accountSelctor', () => {
let accountSelector = null;
const currentUser = {
accounts: [
{
id: 1,
name: 'Chatwoot',
role: 'administrator',
},
{
id: 2,
name: 'GitX',
role: 'agent',
},
],
};
let actions = null;
let modules = null;
beforeEach(() => {
accountSelector = mount(AccountSelector, {
global: {
plugins: [store],
components: {
'woot-modal': WootModal,
'woot-modal-header': WootModalHeader,
'fluent-icon': FluentIcon,
},
stubs: {
WootButton: { template: '<button />' },
// override global stub
WootModalHeader: false,
actions = {};
modules = {
auth: {
getters: {
getCurrentAccountId: () => 1,
getCurrentUser: () => currentUser,
},
},
props: { showAccountModal: true },
globalConfig: {
getters: {
'globalConfig/get': () => ({ createNewAccountFromDashboard: false }),
},
},
};
let store = new Vuex.Store({ actions, modules });
accountSelector = mount(AccountSelector, {
store,
localVue,
i18n: i18nConfig,
propsData: { showAccountModal: true },
stubs: { WootButton: { template: '<button />' } },
});
});
it('title and sub title exist', () => {
const headerComponent = accountSelector.findComponent(WootModalHeader);
const title = headerComponent.find('[data-test-id="modal-header-title"]');
const title = headerComponent.findComponent({ ref: 'modalHeaderTitle' });
expect(title.text()).toBe('Switch Account');
const content = headerComponent.find(
'[data-test-id="modal-header-content"]'
);
const content = headerComponent.findComponent({
ref: 'modalHeaderContent',
});
expect(content.text()).toBe('Select an account from the following list');
});
@@ -1,10 +1,27 @@
import { shallowMount } from '@vue/test-utils';
import { createStore } from 'vuex';
import AgentDetails from '../AgentDetails.vue';
import { createLocalVue, shallowMount } from '@vue/test-utils';
import Vuex from 'vuex';
import VueI18n from 'vue-i18n';
import i18n from 'dashboard/i18n';
import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue';
import WootButton from 'dashboard/components/ui/WootButton.vue';
const localVue = createLocalVue();
localVue.use(Vuex);
localVue.use(VueI18n);
localVue.component('thumbnail', Thumbnail);
localVue.component('woot-button', WootButton);
localVue.component('woot-button', WootButton);
localVue.use(VTooltip, {
defaultHtml: false,
});
describe('AgentDetails', () => {
const i18nConfig = new VueI18n({
locale: 'en',
messages: i18n,
});
describe('agentDetails', () => {
const currentUser = {
name: 'Neymar Junior',
avatar_url: '',
@@ -12,46 +29,37 @@ describe('AgentDetails', () => {
};
const currentRole = 'agent';
let store = null;
let actions = null;
let modules = null;
let agentDetails = null;
const mockTooltipDirective = {
mounted: (el, binding) => {
// You can mock the behavior here if necessary
el.setAttribute('data-tooltip', binding.value || '');
},
};
beforeEach(() => {
store = createStore({
modules: {
auth: {
namespaced: false,
getters: {
getCurrentUser: () => currentUser,
getCurrentRole: () => currentRole,
getCurrentUserAvailability: () => currentUser.availability_status,
},
actions = {};
modules = {
auth: {
getters: {
getCurrentUser: () => currentUser,
getCurrentRole: () => currentRole,
getCurrentUserAvailability: () => currentUser.availability_status,
},
},
};
store = new Vuex.Store({
actions,
modules,
});
agentDetails = shallowMount(AgentDetails, {
global: {
plugins: [store],
components: {
Thumbnail,
WootButton,
},
directives: {
tooltip: mockTooltipDirective, // Mocking the tooltip directive
},
stubs: { WootButton: { template: '<button><slot /></button>' } },
},
store,
localVue,
i18n: i18nConfig,
});
});
it('shows the correct agent status', () => {
expect(agentDetails.findComponent(Thumbnail).vm.status).toBe('online');
it(' the agent status', () => {
expect(agentDetails.find('thumbnail-stub').vm.status).toBe('online');
});
it('agent thumbnail exists', () => {
@@ -1,7 +1,20 @@
import { shallowMount } from '@vue/test-utils';
import { createStore } from 'vuex';
import FluentIcon from 'shared/components/FluentIcon/DashboardIcon.vue';
import NotificationBell from '../NotificationBell.vue';
import { createLocalVue, shallowMount } from '@vue/test-utils';
import Vuex from 'vuex';
import VueI18n from 'vue-i18n';
import FluentIcon from 'shared/components/FluentIcon/DashboardIcon.vue';
import i18n from 'dashboard/i18n';
const localVue = createLocalVue();
localVue.use(Vuex);
localVue.use(VueI18n);
localVue.component('fluent-icon', FluentIcon);
const i18nConfig = new VueI18n({
locale: 'en',
messages: i18n,
});
const $route = {
name: 'notifications_index',
@@ -20,51 +33,43 @@ describe('notificationBell', () => {
};
modules = {
auth: {
namespaced: false,
getters: {
getCurrentAccountId: () => accountId,
},
},
notifications: {
namespaced: false,
getters: {
'notifications/getMeta': () => notificationMetadata,
},
},
};
store = createStore({
store = new Vuex.Store({
actions,
modules,
});
});
it('it should return unread count 19', () => {
it('it should return unread count 19 ', () => {
const wrapper = shallowMount(NotificationBell, {
global: {
plugins: [store],
mocks: {
$route,
},
components: {
'fluent-icon': FluentIcon,
},
localVue,
i18n: i18nConfig,
store,
mocks: {
$route,
},
});
expect(wrapper.vm.unreadCount).toBe('19');
});
it('it should return unread count 99+', async () => {
it('it should return unread count 99+ ', async () => {
notificationMetadata.unreadCount = 100;
const wrapper = shallowMount(NotificationBell, {
global: {
plugins: [store],
mocks: {
$route,
},
components: {
'fluent-icon': FluentIcon,
},
localVue,
i18n: i18nConfig,
store,
mocks: {
$route,
},
});
expect(wrapper.vm.unreadCount).toBe('99+');
@@ -72,14 +77,11 @@ describe('notificationBell', () => {
it('isNotificationPanelActive', async () => {
const notificationBell = shallowMount(NotificationBell, {
global: {
plugins: [store],
mocks: {
$route,
},
components: {
'fluent-icon': FluentIcon,
},
store,
localVue,
i18n: i18nConfig,
mocks: {
$route,
},
});
@@ -1,6 +1,9 @@
import { mount } from '@vue/test-utils';
import { createStore } from 'vuex';
import AvailabilityStatus from '../AvailabilityStatus.vue';
import { createLocalVue, mount } from '@vue/test-utils';
import Vuex from 'vuex';
import VueI18n from 'vue-i18n';
import FloatingVue from 'floating-vue';
import WootButton from 'dashboard/components/ui/WootButton.vue';
import WootDropdownItem from 'shared/components/ui/dropdown/DropdownItem.vue';
import WootDropdownMenu from 'shared/components/ui/dropdown/DropdownMenu.vue';
@@ -8,64 +11,70 @@ import WootDropdownHeader from 'shared/components/ui/dropdown/DropdownHeader.vue
import WootDropdownDivider from 'shared/components/ui/dropdown/DropdownDivider.vue';
import FluentIcon from 'shared/components/FluentIcon/DashboardIcon.vue';
import i18n from 'dashboard/i18n';
const localVue = createLocalVue();
localVue.use(FloatingVue, {
html: false,
});
localVue.use(Vuex);
localVue.use(VueI18n);
localVue.component('woot-button', WootButton);
localVue.component('woot-dropdown-header', WootDropdownHeader);
localVue.component('woot-dropdown-menu', WootDropdownMenu);
localVue.component('woot-dropdown-divider', WootDropdownDivider);
localVue.component('woot-dropdown-item', WootDropdownItem);
localVue.component('fluent-icon', FluentIcon);
const i18nConfig = new VueI18n({ locale: 'en', messages: i18n });
describe('AvailabilityStatus', () => {
const currentAvailability = 'online';
const currentAccountId = '1';
const currentUserAutoOffline = false;
let store = null;
let actions = null;
let modules = null;
let availabilityStatus = null;
beforeEach(() => {
actions = {
updateAvailability: vi.fn(() => Promise.resolve()),
updateAvailability: vi.fn(() => {
return Promise.resolve();
}),
};
store = createStore({
modules: {
auth: {
namespaced: false,
getters: {
getCurrentUserAvailability: () => currentAvailability,
getCurrentAccountId: () => currentAccountId,
getCurrentUserAutoOffline: () => currentUserAutoOffline,
},
modules = {
auth: {
getters: {
getCurrentUserAvailability: () => currentAvailability,
getCurrentAccountId: () => currentAccountId,
getCurrentUserAutoOffline: () => currentUserAutoOffline,
},
},
actions,
};
store = new Vuex.Store({ actions, modules });
availabilityStatus = mount(AvailabilityStatus, {
store,
localVue,
i18n: i18nConfig,
stubs: { WootSwitch: { template: '<button />' } },
});
});
it('dispatches an action when user changes status', async () => {
const wrapper = mount(AvailabilityStatus, {
global: {
plugins: [store],
components: {
WootButton,
WootDropdownItem,
WootDropdownMenu,
WootDropdownHeader,
WootDropdownDivider,
FluentIcon,
},
stubs: {
WootSwitch: { template: '<button />' },
},
},
});
await availabilityStatus;
availabilityStatus
.findAll('.status-change--dropdown-button')
.at(2)
.trigger('click');
// Ensure that the dropdown menu is opened
await wrapper.vm.openStatusMenu();
// Simulate the user clicking the 3rd button (offline status)
const buttons = wrapper.findAll('.status-change--dropdown-button');
expect(buttons.length).toBeGreaterThan(0); // Ensure buttons exist
await buttons[2].trigger('click');
expect(actions.updateAvailability).toHaveBeenCalledTimes(1);
expect(actions.updateAvailability.mock.calls[0][1]).toEqual({
availability: 'offline',
account_id: currentAccountId,
});
expect(actions.updateAvailability).toBeCalledWith(
expect.any(Object),
{ availability: 'offline', account_id: currentAccountId },
undefined
);
});
});
@@ -7,8 +7,5 @@ exports[`SidemenuIcon > matches snapshot 1`] = `
icon="list"
size="small"
variant="clear"
>
</button>
/>
`;
@@ -84,10 +84,10 @@ const shouldShowEmptyState = computed(() => {
<slot name="search">
<DropdownSearch
v-if="enableSearch"
v-model="searchTerm"
:input-value="searchTerm"
:input-placeholder="inputPlaceholder"
:show-clear-filter="showClearFilter"
@update:model-value="onSearch"
@input="onSearch"
@remove="$emit('removeFilter')"
/>
</slot>
@@ -1,6 +1,10 @@
<script setup>
import { defineEmits, defineModel } from 'vue';
import { defineEmits } from 'vue';
defineProps({
inputValue: {
type: String,
default: '',
},
inputPlaceholder: {
type: String,
default: '',
@@ -11,12 +15,7 @@ defineProps({
},
});
const emit = defineEmits(['remove']);
const value = defineModel({
type: String,
default: '',
});
const emit = defineEmits(['input', 'remove']);
</script>
<template>
@@ -30,15 +29,16 @@ const value = defineModel({
class="text-slate-400 dark:text-slate-400 flex-shrink-0"
/>
<input
v-model="value"
:placeholder="inputPlaceholder"
type="text"
class="w-full mb-0 text-sm bg-white dark:bg-slate-800 text-slate-800 dark:text-slate-75 reset-base"
:placeholder="inputPlaceholder"
:value="inputValue"
@input="emit('input', $event.target.value)"
/>
</div>
<!-- Clear filter button -->
<woot-button
v-if="!modelValue && showClearFilter"
v-if="!inputValue && showClearFilter"
size="small"
variant="clear"
color-scheme="primary"
@@ -1,10 +1,9 @@
<script>
export default {
props: {
teams: { type: Array, required: true },
modelValue: { type: Object, required: true },
},
emits: ['update:modelValue'],
// The value types are dynamic, hence prop validation removed to work with our action schema
// eslint-disable-next-line vue/require-prop-types
props: ['teams', 'value'],
emits: ['input'],
data() {
return {
selectedTeams: [],
@@ -12,13 +11,13 @@ export default {
};
},
mounted() {
const { team_ids: teamIds } = this.modelValue;
const { team_ids: teamIds } = this.value;
this.selectedTeams = teamIds;
this.message = this.modelValue.message;
this.message = this.value.message;
},
methods: {
updateValue() {
this.$emit('update:modelValue', {
this.$emit('input', {
team_ids: this.selectedTeams.map(team => team.id),
message: this.message,
});
@@ -13,12 +13,7 @@ const props = defineProps({
},
});
const emit = defineEmits([
'recorderProgressChanged',
'finishRecord',
'pause',
'play',
]);
const emit = defineEmits(['recorderProgressChanged', 'finishRecord']);
const waveformContainer = ref(null);
const wavesurfer = ref(null);
@@ -52,9 +47,6 @@ const initWaveSurfer = () => {
],
});
wavesurfer.value.on('pause', () => emit('pause'));
wavesurfer.value.on('play', () => emit('play'));
record.value = wavesurfer.value.plugins[0];
wavesurfer.value.on('finish', () => {
@@ -114,9 +106,11 @@ onUnmounted(() => {
}
});
defineExpose({ playPause, stopRecording, record });
defineExpose({ playPause, stopRecording });
</script>
<template>
<div ref="waveformContainer" class="w-full p-1" />
<div class="w-full">
<div ref="waveformContainer" />
</div>
</template>
@@ -1,5 +1,4 @@
<script>
import { ref } from 'vue';
import { useUISettings } from 'dashboard/composables/useUISettings';
import { useKeyboardEvents } from 'dashboard/composables/useKeyboardEvents';
import FileUpload from 'vue-upload-component';
@@ -35,7 +34,7 @@ export default {
},
recordingAudioDurationText: {
type: String,
default: '00:00',
default: '',
},
// inbox prop is used in /mixins/inboxMixin,
// remove this props when refactoring to composable if not needed
@@ -123,8 +122,6 @@ export default {
const { setSignatureFlagForInbox, fetchSignatureFlagFromUISettings } =
useUISettings();
const uploadRef = ref(false);
const keyboardEvents = {
'Alt+KeyA': {
action: () => {
@@ -146,7 +143,6 @@ export default {
return {
setSignatureFlagForInbox,
fetchSignatureFlagFromUISettings,
uploadRef,
};
},
computed: {
@@ -259,13 +255,13 @@ export default {
v-tooltip.top-end="$t('CONVERSATION.REPLYBOX.TIP_EMOJI_ICON')"
:title="$t('CONVERSATION.REPLYBOX.TIP_EMOJI_ICON')"
icon="emoji"
emoji="😊"
color-scheme="secondary"
variant="smooth"
size="small"
@click="toggleEmojiPicker"
/>
<FileUpload
ref="uploadRef"
v-tooltip.top-end="$t('CONVERSATION.REPLYBOX.TIP_ATTACH_ICON')"
input-id="conversationAttachment"
:size="4096 * 4096"
@@ -284,6 +280,7 @@ export default {
class-names="button--upload"
:title="$t('CONVERSATION.REPLYBOX.TIP_ATTACH_ICON')"
icon="attach"
emoji="📎"
color-scheme="secondary"
variant="smooth"
size="small"
@@ -293,6 +290,7 @@ export default {
v-if="showAudioRecorderButton"
v-tooltip.top-end="$t('CONVERSATION.REPLYBOX.TIP_AUDIORECORDER_ICON')"
:icon="!isRecordingAudio ? 'microphone' : 'microphone-off'"
emoji="🎤"
:color-scheme="!isRecordingAudio ? 'secondary' : 'alert'"
variant="smooth"
size="small"
@@ -302,6 +300,7 @@ export default {
v-if="showEditorToggle"
v-tooltip.top-end="$t('CONVERSATION.REPLYBOX.TIP_FORMAT_ICON')"
icon="quote"
emoji="🖊️"
color-scheme="secondary"
variant="smooth"
size="small"
@@ -310,6 +309,7 @@ export default {
<woot-button
v-if="showAudioPlayStopButton"
:icon="audioRecorderPlayStopIcon"
emoji="🎤"
color-scheme="secondary"
variant="smooth"
size="small"
@@ -350,7 +350,7 @@ export default {
/>
<transition name="modal-fade">
<div
v-show="uploadRef && uploadRef.dropActive"
v-show="$refs.uploadRef && $refs.uploadRef.dropActive"
class="fixed top-0 bottom-0 left-0 right-0 z-20 flex flex-col items-center justify-center w-full h-full gap-2 text-slate-900 dark:text-slate-50 bg-modal-backdrop-light dark:bg-modal-backdrop-dark"
>
<fluent-icon icon="cloud-backup" size="40" />
@@ -1,7 +1,6 @@
import lamejs from '@breezystack/lamejs';
const writeString = (view, offset, string) => {
// eslint-disable-next-line no-plusplus
for (let i = 0; i < string.length; i++) {
view.setUint8(offset + i, string.charCodeAt(i));
}
@@ -29,9 +28,7 @@ const bufferToWav = async (buffer, numChannels, sampleRate) => {
// WAV Data
const offset = 44;
// eslint-disable-next-line no-plusplus
for (let i = 0; i < buffer.length; i++) {
// eslint-disable-next-line no-plusplus
for (let channel = 0; channel < numChannels; channel++) {
const sample = Math.max(
-1,
@@ -478,7 +478,6 @@ export default {
</div>
<ul class="conversation-panel">
<transition name="slide-up">
<!-- eslint-disable-next-line vue/require-toggle-inside-transition -->
<li class="min-h-[4rem]">
<span v-if="shouldShowSpinner" class="spinner message" />
</li>
@@ -19,7 +19,7 @@ import MessageSignatureMissingAlert from './MessageSignatureMissingAlert.vue';
import Banner from 'dashboard/components/ui/Banner.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';
import WootAudioRecorder from 'dashboard/components/widgets/WootWriter/AudioRecorder.vue';
import { AUDIO_FORMATS } from 'shared/constants/messages';
import { BUS_EVENTS } from 'shared/constants/busEvents';
import {
@@ -50,20 +50,20 @@ const EmojiInput = defineAsyncComponent(
export default {
components: {
ArticleSearchPopover,
AttachmentPreview,
AudioRecorder,
Banner,
CannedResponse,
EmojiInput,
MessageSignatureMissingAlert,
ReplyBottomPanel,
ReplyEmailHead,
CannedResponse,
ReplyToMessage,
ReplyTopPanel,
ResizableTextArea,
WhatsappTemplates,
AttachmentPreview,
ReplyTopPanel,
ReplyEmailHead,
ReplyBottomPanel,
WootMessageEditor,
WootAudioRecorder,
Banner,
WhatsappTemplates,
MessageSignatureMissingAlert,
ArticleSearchPopover,
},
mixins: [inboxMixin, fileUploadMixin, keyboardEventListenerMixins],
emits: ['update:popoutReplyBox', 'togglePopout'],
@@ -115,7 +115,6 @@ export default {
showVariablesMenu: false,
newConversationModalActive: false,
showArticleSearchPopover: false,
hasRecordedAudio: false,
};
},
computed: {
@@ -280,6 +279,12 @@ export default {
hasAttachments() {
return this.attachedFiles.length;
},
hasRecordedAudio() {
return (
this.$refs.audioRecorderInput &&
this.$refs.audioRecorderInput.hasAudio()
);
},
isRichEditorEnabled() {
return this.isAWebWidgetInbox || this.isAnEmailChannel;
},
@@ -359,7 +364,7 @@ export default {
return AUDIO_FORMATS.MP3;
}
if (this.isAPIInbox) {
return AUDIO_FORMATS.MP3;
return AUDIO_FORMATS.OGG;
}
return AUDIO_FORMATS.WAV;
},
@@ -798,14 +803,19 @@ export default {
this.attachedFiles = [];
this.isRecordingAudio = false;
this.resetReplyToMessage();
this.resetAudioRecorderInput();
},
clearEmailField() {
this.ccEmails = '';
this.bccEmails = '';
this.toEmails = '';
},
clearRecorder() {
this.isRecordingAudio = false;
// Only clear the recorded audio when we click toggle button.
this.attachedFiles = this.attachedFiles.filter(
file => !file?.isRecordedAudio
);
},
toggleEmojiPicker() {
this.showEmojiPicker = !this.showEmojiPicker;
},
@@ -813,18 +823,17 @@ export default {
this.isRecordingAudio = !this.isRecordingAudio;
this.isRecorderAudioStopped = !this.isRecordingAudio;
if (!this.isRecordingAudio) {
this.resetAudioRecorderInput();
this.clearRecorder();
}
},
toggleAudioRecorderPlayPause() {
if (!this.isRecordingAudio) {
return;
}
if (!this.isRecorderAudioStopped) {
this.isRecorderAudioStopped = true;
this.$refs.audioRecorderInput.stopRecording();
} else if (this.isRecorderAudioStopped) {
this.$refs.audioRecorderInput.playPause();
if (this.isRecordingAudio) {
if (!this.isRecorderAudioStopped) {
this.isRecorderAudioStopped = true;
this.$refs.audioRecorderInput.stopRecording();
} else if (this.isRecorderAudioStopped) {
this.$refs.audioRecorderInput.playPause();
}
}
},
hideEmojiPicker() {
@@ -851,9 +860,13 @@ export default {
onRecordProgressChanged(duration) {
this.recordingAudioDurationText = duration;
},
onStateRecorderChanged(state) {
this.recordingAudioState = state;
if (state && 'notallowederror'.includes(state)) {
this.toggleAudioRecorder();
}
},
onFinishRecorder(file) {
this.recordingAudioState = 'stopped';
this.hasRecordedAudio = true;
// Added a new key isRecordedAudio to the file to find it's and recorded audio
// Because to filter and show only non recorded audio and other attachments
const autoRecordedFile = {
@@ -1054,16 +1067,6 @@ export default {
toggleInsertArticle() {
this.showArticleSearchPopover = !this.showArticleSearchPopover;
},
resetAudioRecorderInput() {
this.recordingAudioDurationText = '00:00';
this.isRecordingAudio = false;
this.recordingAudioState = '';
this.hasRecordedAudio = false;
// Only clear the recorded audio when we click toggle button.
this.attachedFiles = this.attachedFiles.filter(
file => !file?.isRecordedAudio
);
},
},
};
</script>
@@ -1119,14 +1122,13 @@ export default {
v-model:bcc-emails="bccEmails"
v-model:to-emails="toEmails"
/>
<AudioRecorder
<WootAudioRecorder
v-if="showAudioRecorderEditor"
ref="audioRecorderInput"
:audio-record-format="audioRecordFormat"
@recorder-progress-changed="onRecordProgressChanged"
@state-recorder-changed="onStateRecorderChanged"
@finish-record="onFinishRecorder"
@play="recordingAudioState = 'playing'"
@pause="recordingAudioState = 'paused'"
/>
<ResizableTextArea
v-else-if="!showRichContentEditor"
@@ -1167,11 +1169,7 @@ export default {
@clear-selection="clearEditorSelection"
/>
</div>
<div
v-if="hasAttachments && !showAudioRecorderEditor"
class="attachment-preview-box"
@paste="onPaste"
>
<div v-if="hasAttachments" class="attachment-preview-box" @paste="onPaste">
<AttachmentPreview
class="flex-col mt-4"
:attachments="attachedFiles"
@@ -1,5 +1,5 @@
<script>
import { Letter } from 'vue-letter';
import Letter from 'vue-letter';
import GalleryView from '../components/GalleryView.vue';
export default {
@@ -1,7 +1,11 @@
import { mount } from '@vue/test-utils';
import { createStore } from 'vuex';
import MoreActions from '../MoreActions.vue';
import { createLocalVue, mount } from '@vue/test-utils';
import Vuex from 'vuex';
import VueI18n from 'vue-i18n';
import FloatingVue from 'floating-vue';
import Button from 'dashboard/components/buttons/Button.vue';
import i18n from 'dashboard/i18n';
import FluentIcon from 'shared/components/FluentIcon/DashboardIcon.vue';
import MoreActions from '../MoreActions.vue';
vi.mock('shared/helpers/mitt', () => ({
emitter: {
@@ -11,67 +15,75 @@ vi.mock('shared/helpers/mitt', () => ({
},
}));
const mockDirective = {
mounted: () => {},
import { emitter } from 'shared/helpers/mitt';
const localVue = createLocalVue();
localVue.use(Vuex);
localVue.use(VueI18n);
localVue.use(FloatingVue);
localVue.component('fluent-icon', FluentIcon);
localVue.component('woot-button', Button);
localVue.prototype.$emitter = {
emit: vi.fn(),
on: vi.fn(),
off: vi.fn(),
};
import { emitter } from 'shared/helpers/mitt';
const i18nConfig = new VueI18n({ locale: 'en', messages: i18n });
describe('MoveActions', () => {
let currentChat = { id: 8, muted: false };
let store = null;
let state = null;
let muteConversation = null;
let unmuteConversation = null;
let modules = null;
let getters = null;
let store = null;
let moreActions = null;
beforeEach(() => {
state = {
authenticated: true,
currentChat,
};
muteConversation = vi.fn(() => Promise.resolve());
unmuteConversation = vi.fn(() => Promise.resolve());
store = createStore({
state: {
authenticated: true,
currentChat,
},
getters: {
getSelectedChat: () => currentChat,
},
modules: {
conversations: {
namespaced: false,
actions: { muteConversation, unmuteConversation },
},
modules = {
conversations: { actions: { muteConversation, unmuteConversation } },
};
getters = { getSelectedChat: () => currentChat };
store = new Vuex.Store({ state, modules, getters });
moreActions = mount(MoreActions, {
store,
localVue,
i18n: i18nConfig,
stubs: {
WootModal: { template: '<div><slot/> </div>' },
WootModalHeader: { template: '<div><slot/> </div>' },
},
});
});
const createWrapper = () =>
mount(MoreActions, {
global: {
plugins: [store],
components: {
'fluent-icon': FluentIcon,
},
directives: {
'on-clickaway': mockDirective,
},
},
});
describe('muting discussion', () => {
it('triggers "muteConversation"', async () => {
const wrapper = createWrapper();
await wrapper.find('button:first-child').trigger('click');
await moreActions.find('button:first-child').trigger('click');
expect(muteConversation).toHaveBeenCalledTimes(1);
expect(muteConversation).toHaveBeenCalledWith(
expect.any(Object), // First argument is the Vuex context object
currentChat.id // Second argument is the ID of the conversation
expect(muteConversation).toBeCalledWith(
expect.any(Object),
currentChat.id,
undefined
);
});
it('shows alert', async () => {
const wrapper = createWrapper();
await wrapper.find('button:first-child').trigger('click');
await moreActions.find('button:first-child').trigger('click');
expect(emitter.emit).toBeCalledWith('newToastMessage', {
message:
@@ -87,19 +99,17 @@ describe('MoveActions', () => {
});
it('triggers "unmuteConversation"', async () => {
const wrapper = createWrapper();
await wrapper.find('button:first-child').trigger('click');
await moreActions.find('button:first-child').trigger('click');
expect(unmuteConversation).toHaveBeenCalledTimes(1);
expect(unmuteConversation).toHaveBeenCalledWith(
expect.any(Object), // First argument is the Vuex context object
currentChat.id // Second argument is the ID of the conversation
expect(unmuteConversation).toBeCalledWith(
expect.any(Object),
currentChat.id,
undefined
);
});
it('shows alert', async () => {
const wrapper = createWrapper();
await wrapper.find('button:first-child').trigger('click');
await moreActions.find('button:first-child').trigger('click');
expect(emitter.emit).toBeCalledWith('newToastMessage', {
message: 'This contact is unblocked successfully.',
@@ -1,5 +1,5 @@
import { emitter } from 'shared/helpers/mitt';
import analyticsHelper from 'dashboard/helper/AnalyticsHelper/index';
import analyticsHelper from '/dashboard/helper/AnalyticsHelper/index';
/**
* Custom hook to track events
@@ -1,7 +1,6 @@
import { shallowMount } from '@vue/test-utils';
import { emitter } from 'shared/helpers/mitt';
import { useEmitter } from '../emitter';
import { defineComponent } from 'vue';
vi.mock('shared/helpers/mitt', () => ({
emitter: {
@@ -11,34 +10,31 @@ vi.mock('shared/helpers/mitt', () => ({
}));
describe('useEmitter', () => {
let wrapper;
const eventName = 'my-event';
const callback = vi.fn();
let wrapper;
const TestComponent = defineComponent({
setup() {
return {
cleanup: useEmitter(eventName, callback),
};
},
template: '<div>Hello world</div>',
});
beforeEach(() => {
wrapper = shallowMount(TestComponent);
});
afterEach(() => {
vi.clearAllMocks();
wrapper = shallowMount({
template: `
<div>
Hello world
</div>
`,
setup() {
return {
cleanup: useEmitter(eventName, callback),
};
},
});
});
it('should add an event listener on mount', () => {
expect(emitter.on).toHaveBeenCalledWith(eventName, callback);
});
it('should remove the event listener when the component is unmounted', async () => {
await wrapper.unmount();
it('should remove the event listener when the component is unmounted', () => {
wrapper.destroy();
expect(emitter.off).toHaveBeenCalledWith(eventName, callback);
});
@@ -1,26 +1,20 @@
import { getCurrentInstance } from 'vue';
import { emitter } from 'shared/helpers/mitt';
import analyticsHelper from 'dashboard/helper/AnalyticsHelper';
import { useTrack, useAlert } from '../index';
vi.mock('vue', () => ({
getCurrentInstance: vi.fn(),
}));
vi.mock('shared/helpers/mitt', () => ({
emitter: {
emit: vi.fn(),
},
}));
vi.mock('dashboard/helper/AnalyticsHelper/index', async importOriginal => {
const actual = await importOriginal();
actual.default = {
track: vi.fn(),
};
return actual;
});
describe('useTrack', () => {
it('should call analyticsHelper.track and return a function', () => {
const eventArgs = ['event-name', { some: 'data' }];
useTrack(...eventArgs);
expect(analyticsHelper.track).toHaveBeenCalledWith(...eventArgs);
it('should return a function', () => {
const track = useTrack();
expect(typeof track).toBe('function');
});
});
@@ -4,20 +4,14 @@ import {
useStoreGetters,
useMapGetter,
} from 'dashboard/composables/store';
import { useAlert, useTrack } from 'dashboard/composables';
import { useI18n } from 'vue-i18n';
import OpenAPI from 'dashboard/api/integrations/openapi';
import analyticsHelper from 'dashboard/helper/AnalyticsHelper/index';
vi.mock('dashboard/composables/store');
vi.mock('dashboard/composables');
vi.mock('vue-i18n');
vi.mock('dashboard/api/integrations/openapi');
vi.mock('dashboard/helper/AnalyticsHelper/index', async importOriginal => {
const actual = await importOriginal();
actual.default = {
track: vi.fn(),
};
return actual;
});
vi.mock('dashboard/helper/AnalyticsHelper/events', () => ({
OPEN_AI_EVENTS: {
TEST_EVENT: 'open_ai_test_event',
@@ -46,7 +40,9 @@ describe('useAI', () => {
};
return { value: mockValues[getter] };
});
useTrack.mockReturnValue(vi.fn());
useI18n.mockReturnValue({ t: vi.fn() });
useAlert.mockReturnValue(vi.fn());
});
it('initializes computed properties correctly', async () => {
@@ -82,12 +78,13 @@ describe('useAI', () => {
});
it('records analytics correctly', async () => {
// const mockTrack = analyticsHelper.track;
const mockTrack = vi.fn();
useTrack.mockReturnValue(mockTrack);
const { recordAnalytics } = useAI();
await recordAnalytics('TEST_EVENT', { data: 'test' });
expect(analyticsHelper.track).toHaveBeenCalledWith('open_ai_test_event', {
expect(mockTrack).toHaveBeenCalledWith('open_ai_test_event', {
type: 'TEST_EVENT',
data: 'test',
});
@@ -1,15 +1,14 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { ref } from 'vue';
import { describe, it, expect, vi } from 'vitest';
import { useAccount } from '../useAccount';
import { useRoute } from 'vue-router';
import { useStoreGetters } from 'dashboard/composables/store';
vi.mock('vue-router');
vi.mock('dashboard/composables/store');
describe('useAccount', () => {
beforeEach(() => {
useRoute.mockReturnValue({
params: {
accountId: 123,
},
useStoreGetters.mockReturnValue({
getCurrentAccountId: ref(123),
});
});
@@ -1,7 +1,7 @@
import { useAutomation } from '../useAutomation';
import { useStoreGetters, useMapGetter } from 'dashboard/composables/store';
import { useAlert } from 'dashboard/composables';
import { useI18n } from 'vue-i18n';
import { useI18n } from '../useI18n';
import * as automationHelper from 'dashboard/helper/automationHelper';
import {
customAttributes,
@@ -20,7 +20,7 @@ import { MESSAGE_CONDITION_VALUES } from 'dashboard/constants/automation';
vi.mock('dashboard/composables/store');
vi.mock('dashboard/composables');
vi.mock('vue-i18n');
vi.mock('../useI18n');
vi.mock('dashboard/helper/automationHelper');
describe('useAutomation', () => {
@@ -120,8 +120,8 @@ describe('useAutomation', () => {
});
it('appends new condition and action correctly', () => {
const { appendNewCondition, appendNewAction, automation } = useAutomation();
automation.value = {
const { appendNewCondition, appendNewAction } = useAutomation();
const mockAutomation = {
event_name: 'message_created',
conditions: [],
actions: [],
@@ -130,37 +130,36 @@ describe('useAutomation', () => {
automationHelper.getDefaultConditions.mockReturnValue([{}]);
automationHelper.getDefaultActions.mockReturnValue([{}]);
appendNewCondition();
appendNewAction();
appendNewCondition(mockAutomation);
appendNewAction(mockAutomation);
expect(automationHelper.getDefaultConditions).toHaveBeenCalledWith(
'message_created'
);
expect(automationHelper.getDefaultActions).toHaveBeenCalled();
expect(automation.value.conditions).toHaveLength(1);
expect(automation.value.actions).toHaveLength(1);
expect(mockAutomation.conditions).toHaveLength(1);
expect(mockAutomation.actions).toHaveLength(1);
});
it('removes filter and action correctly', () => {
const { removeFilter, removeAction, automation } = useAutomation();
automation.value = {
const { removeFilter, removeAction } = useAutomation();
const mockAutomation = {
conditions: [{ id: 1 }, { id: 2 }],
actions: [{ id: 1 }, { id: 2 }],
};
removeFilter(0);
removeAction(0);
removeFilter(mockAutomation, 0);
removeAction(mockAutomation, 0);
expect(automation.value.conditions).toHaveLength(1);
expect(automation.value.actions).toHaveLength(1);
expect(automation.value.conditions[0].id).toBe(2);
expect(automation.value.actions[0].id).toBe(2);
expect(mockAutomation.conditions).toHaveLength(1);
expect(mockAutomation.actions).toHaveLength(1);
expect(mockAutomation.conditions[0].id).toBe(2);
expect(mockAutomation.actions[0].id).toBe(2);
});
it('resets filter and action correctly', () => {
const { resetFilter, resetAction, automation, automationTypes } =
useAutomation();
automation.value = {
const { resetFilter, resetAction } = useAutomation();
const mockAutomation = {
event_name: 'message_created',
conditions: [
{
@@ -171,37 +170,77 @@ describe('useAutomation', () => {
],
actions: [{ action_name: 'assign_agent', action_params: [1] }],
};
automationTypes.message_created = {
conditions: [
{ key: 'status', filterOperators: [{ value: 'not_equal_to' }] },
],
const mockAutomationTypes = {
message_created: {
conditions: [
{ key: 'status', filterOperators: [{ value: 'not_equal_to' }] },
],
},
};
resetFilter(0, automation.value.conditions[0]);
resetAction(0);
resetFilter(
mockAutomation,
mockAutomationTypes,
0,
mockAutomation.conditions[0]
);
resetAction(mockAutomation, 0);
expect(automation.value.conditions[0].filter_operator).toBe('not_equal_to');
expect(automation.value.conditions[0].values).toBe('');
expect(automation.value.actions[0].action_params).toEqual([]);
expect(mockAutomation.conditions[0].filter_operator).toBe('not_equal_to');
expect(mockAutomation.conditions[0].values).toBe('');
expect(mockAutomation.actions[0].action_params).toEqual([]);
});
it('formats automation correctly', () => {
const { formatAutomation } = useAutomation();
const mockAutomation = {
conditions: [{ attribute_key: 'status', values: ['open'] }],
actions: [{ action_name: 'assign_agent', action_params: [1] }],
};
const mockAutomationTypes = {};
const mockAutomationActionTypes = [
{ key: 'assign_agent', inputType: 'search_select' },
];
automationHelper.getConditionOptions.mockReturnValue([
{ id: 'open', name: 'open' },
]);
automationHelper.getActionOptions.mockReturnValue([
{ id: 1, name: 'Agent 1' },
]);
const result = formatAutomation(
mockAutomation,
customAttributes,
mockAutomationTypes,
mockAutomationActionTypes
);
expect(result.conditions[0].values).toEqual([{ id: 'open', name: 'open' }]);
expect(result.actions[0].action_params).toEqual([
{ id: 1, name: 'Agent 1' },
]);
});
it('manifests custom attributes correctly', () => {
const { manifestCustomAttributes, automationTypes } = useAutomation();
automationTypes.message_created = { conditions: [] };
automationTypes.conversation_created = { conditions: [] };
automationTypes.conversation_updated = { conditions: [] };
automationTypes.conversation_opened = { conditions: [] };
const { manifestCustomAttributes } = useAutomation();
const mockAutomationTypes = {
message_created: { conditions: [] },
conversation_created: { conditions: [] },
conversation_updated: { conditions: [] },
conversation_opened: { conditions: [] },
};
automationHelper.generateCustomAttributeTypes.mockReturnValue([]);
automationHelper.generateCustomAttributes.mockReturnValue([]);
manifestCustomAttributes();
manifestCustomAttributes(mockAutomationTypes);
expect(automationHelper.generateCustomAttributeTypes).toHaveBeenCalledTimes(
2
);
expect(automationHelper.generateCustomAttributes).toHaveBeenCalledTimes(1);
Object.values(automationTypes).forEach(type => {
Object.values(mockAutomationTypes).forEach(type => {
expect(type.conditions).toHaveLength(0);
});
});
@@ -234,8 +273,8 @@ describe('useAutomation', () => {
});
it('handles event change correctly', () => {
const { onEventChange, automation } = useAutomation();
automation.value = {
const { onEventChange } = useAutomation();
const mockAutomation = {
event_name: 'message_created',
conditions: [],
actions: [],
@@ -244,13 +283,13 @@ describe('useAutomation', () => {
automationHelper.getDefaultConditions.mockReturnValue([{}]);
automationHelper.getDefaultActions.mockReturnValue([{}]);
onEventChange();
onEventChange(mockAutomation);
expect(automationHelper.getDefaultConditions).toHaveBeenCalledWith(
'message_created'
);
expect(automationHelper.getDefaultActions).toHaveBeenCalled();
expect(automation.value.conditions).toHaveLength(1);
expect(automation.value.actions).toHaveLength(1);
expect(mockAutomation.conditions).toHaveLength(1);
expect(mockAutomation.actions).toHaveLength(1);
});
});
@@ -1,20 +1,18 @@
import { computed } from 'vue';
import { useRoute } from 'vue-router';
import { useStoreGetters } from 'dashboard/composables/store';
/**
* Composable for account-related operations.
* @returns {Object} An object containing account-related properties and methods.
*/
export function useAccount() {
const getters = useStoreGetters();
/**
* Computed property for the current account ID.
* @type {import('vue').ComputedRef<number>}
*/
const route = useRoute();
const accountId = computed(() => {
return Number(route.params.accountId);
});
const accountId = computed(() => getters.getCurrentAccountId.value);
/**
* Generates an account-scoped URL.
@@ -1,143 +1,313 @@
import { ref, computed } from 'vue';
import { useStoreGetters } from 'dashboard/composables/store';
import { computed } from 'vue';
import { useStoreGetters, useMapGetter } from 'dashboard/composables/store';
import { useAlert } from 'dashboard/composables';
import { useI18n } from 'vue-i18n';
import languages from 'dashboard/components/widgets/conversation/advancedFilterItems/languages';
import countries from 'shared/constants/countries';
import {
generateCustomAttributeTypes,
getActionOptions,
getConditionOptions,
getCustomAttributeInputType,
getDefaultConditions,
getDefaultActions,
filterCustomAttributes,
getStandardAttributeInputType,
isCustomAttribute,
generateCustomAttributes,
} from 'dashboard/helper/automationHelper';
import useAutomationValues from './useAutomationValues';
import {
// AUTOMATION_RULE_EVENTS,
// AUTOMATION_ACTION_TYPES,
AUTOMATIONS,
} from 'dashboard/routes/dashboard/settings/automation/constants.js';
/**
* Composable for handling automation-related functionality.
* @returns {Object} An object containing various automation-related functions and computed properties.
*/
export function useAutomation(startValue = null) {
export function useAutomation() {
const getters = useStoreGetters();
const { t } = useI18n();
const {
booleanFilterOptions,
statusFilterOptions,
getConditionDropdownValues,
getActionDropdownValues,
agents,
campaigns,
contacts,
inboxes,
labels,
teams,
slaPolicies,
} = useAutomationValues();
const agents = useMapGetter('agents/getAgents');
const campaigns = useMapGetter('campaigns/getAllCampaigns');
const contacts = useMapGetter('contacts/getContacts');
const inboxes = useMapGetter('inboxes/getInboxes');
const labels = useMapGetter('labels/getLabels');
const teams = useMapGetter('teams/getTeams');
const slaPolicies = useMapGetter('sla/getSLA');
const automation = ref(startValue);
const automationTypes = structuredClone(AUTOMATIONS);
const eventName = computed(() => automation.value?.event_name);
const booleanFilterOptions = computed(() => [
{ id: true, name: t('FILTER.ATTRIBUTE_LABELS.TRUE') },
{ id: false, name: t('FILTER.ATTRIBUTE_LABELS.FALSE') },
]);
/**
* Handles the event change for an automation.value.
*/
const onEventChange = () => {
automation.value.conditions = getDefaultConditions(eventName.value);
automation.value.actions = getDefaultActions();
};
const statusFilterItems = computed(() => {
return {
open: {
TEXT: t('CHAT_LIST.CHAT_STATUS_FILTER_ITEMS.open.TEXT'),
},
resolved: {
TEXT: t('CHAT_LIST.CHAT_STATUS_FILTER_ITEMS.resolved.TEXT'),
},
pending: {
TEXT: t('CHAT_LIST.CHAT_STATUS_FILTER_ITEMS.pending.TEXT'),
},
snoozed: {
TEXT: t('CHAT_LIST.CHAT_STATUS_FILTER_ITEMS.snoozed.TEXT'),
},
all: {
TEXT: t('CHAT_LIST.CHAT_STATUS_FILTER_ITEMS.all.TEXT'),
},
};
});
/**
* Appends a new condition to the automation.value.
*/
const appendNewCondition = () => {
const defaultCondition = getDefaultConditions(eventName.value);
automation.value.conditions = [
...automation.value.conditions,
...defaultCondition,
const statusFilterOptions = computed(() => {
const statusFilters = statusFilterItems.value;
return [
...Object.keys(statusFilters).map(status => ({
id: status,
name: statusFilters[status].TEXT,
})),
{ id: 'all', name: t('CHAT_LIST.FILTER_ALL') },
];
};
});
/**
* Appends a new action to the automation.value.
* Handles the event change for an automation.
* @param {Object} automation - The automation object to update.
*/
const appendNewAction = () => {
const defaultAction = getDefaultActions();
automation.value.actions = [...automation.value.actions, ...defaultAction];
const onEventChange = automation => {
automation.conditions = getDefaultConditions(automation.event_name);
automation.actions = getDefaultActions();
};
/**
* Removes a filter from the automation.value.
* Gets the condition dropdown values for a given type.
* @param {string} type - The type of condition.
* @returns {Array} An array of condition dropdown values.
*/
const getConditionDropdownValues = type => {
return getConditionOptions({
agents: agents.value,
booleanFilterOptions: booleanFilterOptions.value,
campaigns: campaigns.value,
contacts: contacts.value,
customAttributes: getters['attributes/getAttributes'].value,
inboxes: inboxes.value,
statusFilterOptions: statusFilterOptions.value,
teams: teams.value,
languages,
countries,
type,
});
};
/**
* Appends a new condition to the automation.
* @param {Object} automation - The automation object to update.
*/
const appendNewCondition = automation => {
automation.conditions.push(...getDefaultConditions(automation.event_name));
};
/**
* Appends a new action to the automation.
* @param {Object} automation - The automation object to update.
*/
const appendNewAction = automation => {
automation.actions.push(...getDefaultActions());
};
/**
* Removes a filter from the automation.
* @param {Object} automation - The automation object to update.
* @param {number} index - The index of the filter to remove.
*/
const removeFilter = index => {
if (automation.value.conditions.length <= 1) {
const removeFilter = (automation, index) => {
if (automation.conditions.length <= 1) {
useAlert(t('AUTOMATION.CONDITION.DELETE_MESSAGE'));
} else {
automation.value.conditions = automation.value.conditions.filter(
(_, i) => i !== index
);
automation.conditions.splice(index, 1);
}
};
/**
* Removes an action from the automation.value.
* Removes an action from the automation.
* @param {Object} automation - The automation object to update.
* @param {number} index - The index of the action to remove.
*/
const removeAction = index => {
if (automation.value.actions.length <= 1) {
const removeAction = (automation, index) => {
if (automation.actions.length <= 1) {
useAlert(t('AUTOMATION.ACTION.DELETE_MESSAGE'));
} else {
automation.value.actions = automation.value.actions.filter(
(_, i) => i !== index
);
automation.actions.splice(index, 1);
}
};
/**
* Resets a filter in the automation.value.
* Resets a filter in the automation.
* @param {Object} automation - The automation object to update.
* @param {Object} automationTypes - The automation types object.
* @param {number} index - The index of the filter to reset.
* @param {Object} currentCondition - The current condition object.
*/
const resetFilter = (index, currentCondition) => {
const newConditions = [...automation.value.conditions];
newConditions[index] = {
...newConditions[index],
filter_operator: automationTypes[eventName.value].conditions.find(
condition => condition.key === currentCondition.attribute_key
).filterOperators[0].value,
values: '',
};
automation.value.conditions = newConditions;
const resetFilter = (
automation,
automationTypes,
index,
currentCondition
) => {
automation.conditions[index].filter_operator = automationTypes[
automation.event_name
].conditions.find(
condition => condition.key === currentCondition.attribute_key
).filterOperators[0].value;
automation.conditions[index].values = '';
};
/**
* Resets an action in the automation.value.
* Resets an action in the automation.
* @param {Object} automation - The automation object to update.
* @param {number} index - The index of the action to reset.
*/
const resetAction = index => {
const newActions = [...automation.value.actions];
newActions[index] = {
...newActions[index],
action_params: [],
};
const resetAction = (automation, index) => {
automation.actions[index].action_params = [];
};
automation.value.actions = newActions;
/**
* This function sets the conditions for automation.
* It help to format the conditions for the automation when we open the edit automation modal.
* @param {Object} automation - The automation object containing conditions to manifest.
* @param {Array} allCustomAttributes - List of all custom attributes.
* @param {Object} automationTypes - Object containing automation type definitions.
* @returns {Array} An array of manifested conditions.
*/
const manifestConditions = (
automation,
allCustomAttributes,
automationTypes
) => {
const customAttributes = filterCustomAttributes(allCustomAttributes);
return automation.conditions.map(condition => {
const customAttr = isCustomAttribute(
customAttributes,
condition.attribute_key
);
let inputType = 'plain_text';
if (customAttr) {
inputType = getCustomAttributeInputType(customAttr.type);
} else {
inputType = getStandardAttributeInputType(
automationTypes,
automation.event_name,
condition.attribute_key
);
}
if (inputType === 'plain_text' || inputType === 'date') {
return { ...condition, values: condition.values[0] };
}
if (inputType === 'comma_separated_plain_text') {
return { ...condition, values: condition.values.join(',') };
}
return {
...condition,
query_operator: condition.query_operator || 'and',
values: [...getConditionDropdownValues(condition.attribute_key)].filter(
item => [...condition.values].includes(item.id)
),
};
});
};
/**
* Gets the action dropdown values for a given type.
* @param {string} type - The type of action.
* @returns {Array} An array of action dropdown values.
*/
const getActionDropdownValues = type => {
return getActionOptions({
agents: agents.value,
labels: labels.value,
teams: teams.value,
slaPolicies: slaPolicies.value,
languages,
type,
});
};
/**
* Generates an array of actions for the automation.
* @param {Object} action - The action object.
* @param {Array} automationActionTypes - List of available automation action types.
* @returns {Array|Object} Generated actions array or object based on input type.
*/
const generateActionsArray = (action, automationActionTypes) => {
const params = action.action_params;
const inputType = automationActionTypes.find(
item => item.key === action.action_name
).inputType;
if (inputType === 'multi_select' || inputType === 'search_select') {
return [...getActionDropdownValues(action.action_name)].filter(item =>
[...params].includes(item.id)
);
}
if (inputType === 'team_message') {
return {
team_ids: [...getActionDropdownValues(action.action_name)].filter(
item => [...params[0].team_ids].includes(item.id)
),
message: params[0].message,
};
}
return [...params];
};
/**
* This function sets the actions for automation.
* It help to format the actions for the automation when we open the edit automation modal.
* @param {Object} automation - The automation object containing actions.
* @param {Array} automationActionTypes - List of available automation action types.
* @returns {Array} An array of manifested actions.
*/
const manifestActions = (automation, automationActionTypes) => {
return automation.actions.map(action => ({
...action,
action_params: action.action_params.length
? generateActionsArray(action, automationActionTypes)
: [],
}));
};
/**
* Formats the automation object for use when we edit the automation.
* It help to format the conditions and actions for the automation when we open the edit automation modal.
* @param {Object} automation - The automation object to format.
* @param {Array} allCustomAttributes - List of all custom attributes.
* @param {Object} automationTypes - Object containing automation type definitions.
* @param {Array} automationActionTypes - List of available automation action types.
* @returns {Object} A new object with formatted automation data, including automation conditions and actions.
*/
const formatAutomation = (
automation,
allCustomAttributes,
automationTypes,
automationActionTypes
) => {
return {
...automation,
conditions: manifestConditions(
automation,
allCustomAttributes,
automationTypes
),
actions: manifestActions(automation, automationActionTypes),
};
};
/**
* This function formats the custom attributes for automation types.
* It retrieves custom attributes for conversations and contacts,
* generates custom attribute types, and adds them to the relevant automation types.
* @param {Object} automationTypes - The automation types object to update with custom attributes.
*/
const manifestCustomAttributes = () => {
const manifestCustomAttributes = automationTypes => {
const conversationCustomAttributesRaw = getters[
'attributes/getAttributesByModel'
].value('conversation_attribute');
@@ -160,22 +330,21 @@ export function useAutomation(startValue = null) {
t('AUTOMATION.CONDITION.CONTACT_CUSTOM_ATTR_LABEL')
);
[
'message_created',
'conversation_created',
'conversation_updated',
'conversation_opened',
].forEach(eventToUpdate => {
automationTypes[eventToUpdate].conditions = [
...automationTypes[eventToUpdate].conditions,
...manifestedCustomAttributes,
];
});
automationTypes.message_created.conditions.push(
...manifestedCustomAttributes
);
automationTypes.conversation_created.conditions.push(
...manifestedCustomAttributes
);
automationTypes.conversation_updated.conditions.push(
...manifestedCustomAttributes
);
automationTypes.conversation_opened.conditions.push(
...manifestedCustomAttributes
);
};
return {
automation,
automationTypes,
agents,
campaigns,
contacts,
@@ -193,6 +362,7 @@ export function useAutomation(startValue = null) {
removeAction,
resetFilter,
resetAction,
formatAutomation,
getActionDropdownValues,
manifestCustomAttributes,
};
@@ -1,115 +0,0 @@
import { computed } from 'vue';
import { useI18n } from 'vue-i18n';
import languages from 'dashboard/components/widgets/conversation/advancedFilterItems/languages';
import countries from 'shared/constants/countries';
import { useStoreGetters, useMapGetter } from 'dashboard/composables/store';
import {
getActionOptions,
getConditionOptions,
} from 'dashboard/helper/automationHelper';
/**
* This is a shared composables that holds utilites used to build dropdown and file options
* @returns {Object} An object containing various automation-related functions and computed properties.
*/
export default function useAutomationValues() {
const getters = useStoreGetters();
const { t } = useI18n();
const agents = useMapGetter('agents/getAgents');
const campaigns = useMapGetter('campaigns/getAllCampaigns');
const contacts = useMapGetter('contacts/getContacts');
const inboxes = useMapGetter('inboxes/getInboxes');
const labels = useMapGetter('labels/getLabels');
const teams = useMapGetter('teams/getTeams');
const slaPolicies = useMapGetter('sla/getSLA');
const booleanFilterOptions = computed(() => [
{ id: true, name: t('FILTER.ATTRIBUTE_LABELS.TRUE') },
{ id: false, name: t('FILTER.ATTRIBUTE_LABELS.FALSE') },
]);
const statusFilterItems = computed(() => {
return {
open: {
TEXT: t('CHAT_LIST.CHAT_STATUS_FILTER_ITEMS.open.TEXT'),
},
resolved: {
TEXT: t('CHAT_LIST.CHAT_STATUS_FILTER_ITEMS.resolved.TEXT'),
},
pending: {
TEXT: t('CHAT_LIST.CHAT_STATUS_FILTER_ITEMS.pending.TEXT'),
},
snoozed: {
TEXT: t('CHAT_LIST.CHAT_STATUS_FILTER_ITEMS.snoozed.TEXT'),
},
all: {
TEXT: t('CHAT_LIST.CHAT_STATUS_FILTER_ITEMS.all.TEXT'),
},
};
});
const statusFilterOptions = computed(() => {
const statusFilters = statusFilterItems.value;
return [
...Object.keys(statusFilters).map(status => ({
id: status,
name: statusFilters[status].TEXT,
})),
{ id: 'all', name: t('CHAT_LIST.FILTER_ALL') },
];
});
/**
* Gets the condition dropdown values for a given type.
* @param {string} type - The type of condition.
* @returns {Array} An array of condition dropdown values.
*/
const getConditionDropdownValues = type => {
return getConditionOptions({
agents: agents.value,
booleanFilterOptions: booleanFilterOptions.value,
campaigns: campaigns.value,
contacts: contacts.value,
customAttributes: getters['attributes/getAttributes'].value,
inboxes: inboxes.value,
statusFilterOptions: statusFilterOptions.value,
teams: teams.value,
languages,
countries,
type,
});
};
/**
* Gets the action dropdown values for a given type.
* @param {string} type - The type of action.
* @returns {Array} An array of action dropdown values.
*/
const getActionDropdownValues = type => {
return getActionOptions({
agents: agents.value,
labels: labels.value,
teams: teams.value,
slaPolicies: slaPolicies.value,
languages,
type,
});
};
return {
booleanFilterOptions,
statusFilterItems,
statusFilterOptions,
getConditionDropdownValues,
getActionDropdownValues,
agents,
campaigns,
contacts,
inboxes,
labels,
teams,
slaPolicies,
};
}
@@ -1,129 +0,0 @@
import useAutomationValues from './useAutomationValues';
import {
getCustomAttributeInputType,
filterCustomAttributes,
getStandardAttributeInputType,
isCustomAttribute,
} from 'dashboard/helper/automationHelper';
export function useEditableAutomation() {
const { getConditionDropdownValues, getActionDropdownValues } =
useAutomationValues();
/**
* This function sets the conditions for automation.
* It help to format the conditions for the automation when we open the edit automation modal.
* @param {Object} automation - The automation object containing conditions to manifest.
* @param {Array} allCustomAttributes - List of all custom attributes.
* @param {Object} automationTypes - Object containing automation type definitions.
* @returns {Array} An array of manifested conditions.
*/
const manifestConditions = (
automation,
allCustomAttributes,
automationTypes
) => {
const customAttributes = filterCustomAttributes(allCustomAttributes);
return automation.conditions.map(condition => {
const customAttr = isCustomAttribute(
customAttributes,
condition.attribute_key
);
let inputType = 'plain_text';
if (customAttr) {
inputType = getCustomAttributeInputType(customAttr.type);
} else {
inputType = getStandardAttributeInputType(
automationTypes,
automation.event_name,
condition.attribute_key
);
}
if (inputType === 'plain_text' || inputType === 'date') {
return { ...condition, values: condition.values[0] };
}
if (inputType === 'comma_separated_plain_text') {
return { ...condition, values: condition.values.join(',') };
}
return {
...condition,
query_operator: condition.query_operator || 'and',
values: [...getConditionDropdownValues(condition.attribute_key)].filter(
item => [...condition.values].includes(item.id)
),
};
});
};
/**
* Generates an array of actions for the automation.
* @param {Object} action - The action object.
* @param {Array} automationActionTypes - List of available automation action types.
* @returns {Array|Object} Generated actions array or object based on input type.
*/
const generateActionsArray = (action, automationActionTypes) => {
const params = action.action_params;
const inputType = automationActionTypes.find(
item => item.key === action.action_name
).inputType;
if (inputType === 'multi_select' || inputType === 'search_select') {
return [...getActionDropdownValues(action.action_name)].filter(item =>
[...params].includes(item.id)
);
}
if (inputType === 'team_message') {
return {
team_ids: [...getActionDropdownValues(action.action_name)].filter(
item => [...params[0].team_ids].includes(item.id)
),
message: params[0].message,
};
}
return [...params];
};
/**
* This function sets the actions for automation.
* It help to format the actions for the automation when we open the edit automation modal.
* @param {Object} automation - The automation object containing actions.
* @param {Array} automationActionTypes - List of available automation action types.
* @returns {Array} An array of manifested actions.
*/
const manifestActions = (automation, automationActionTypes) => {
return automation.actions.map(action => ({
...action,
action_params: action.action_params.length
? generateActionsArray(action, automationActionTypes)
: [],
}));
};
/**
* Formats the automation object for use when we edit the automation.
* It help to format the conditions and actions for the automation when we open the edit automation modal.
* @param {Object} automation - The automation object to format.
* @param {Array} allCustomAttributes - List of all custom attributes.
* @param {Object} automationTypes - Object containing automation type definitions.
* @param {Array} automationActionTypes - List of available automation action types.
* @returns {Object} A new object with formatted automation data, including automation conditions and actions.
*/
const formatAutomation = (
automation,
allCustomAttributes,
automationTypes,
automationActionTypes
) => {
return {
...automation,
conditions: manifestConditions(
automation,
allCustomAttributes,
automationTypes
),
actions: manifestActions(automation, automationActionTypes),
};
};
return { formatAutomation };
}
@@ -0,0 +1,36 @@
import Vue from 'vue';
import plugin from '../plugin';
import analyticsHelper from '../index';
vi.spyOn(analyticsHelper, 'init');
vi.spyOn(analyticsHelper, 'track');
describe('Vue Analytics Plugin', () => {
beforeEach(() => {
Vue.use(plugin);
});
it('should call the init method on analyticsHelper once during plugin installation', () => {
expect(analyticsHelper.init).toHaveBeenCalledTimes(1);
});
it('should add the analyticsHelper to the Vue prototype as $analytics', () => {
expect(Vue.prototype.$analytics).toBe(analyticsHelper);
});
it('should add a track method to the Vue prototype as $track', () => {
expect(typeof Vue.prototype.$track).toBe('function');
Vue.prototype.$track('eventName');
expect(analyticsHelper.track)
.toHaveBeenCalledTimes(1)
.toHaveBeenCalledWith('eventName');
});
it('should call the track method on analyticsHelper with the correct event name when $track is called', () => {
const eventName = 'testEvent';
Vue.prototype.$track(eventName);
expect(analyticsHelper.track)
.toHaveBeenCalledTimes(1)
.toHaveBeenCalledWith(eventName);
});
});
@@ -39,13 +39,13 @@ class DashboardAudioNotificationHelper {
};
}
setInstanceValues = ({
setInstanceValues({
currentUser,
alwaysPlayAudioAlert,
alertIfUnreadConversationExist,
audioAlertType,
audioAlertTone,
}) => {
}) {
this.audioAlertType = audioAlertType;
this.playAlertOnlyWhenHidden = !alwaysPlayAudioAlert;
this.alertIfUnreadConversationExist = alertIfUnreadConversationExist;
@@ -58,9 +58,9 @@ class DashboardAudioNotificationHelper {
});
});
initFaviconSwitcher();
};
}
executeRecurringNotification = () => {
executeRecurringNotification() {
if (!window.WOOT_STORE) {
this.clearSetTimeout();
return;
@@ -81,9 +81,9 @@ class DashboardAudioNotificationHelper {
showBadgeOnFavicon();
}
this.clearSetTimeout();
};
}
clearSetTimeout = () => {
clearSetTimeout() {
if (this.recurringNotificationTimer) {
clearTimeout(this.recurringNotificationTimer);
}
@@ -91,9 +91,9 @@ class DashboardAudioNotificationHelper {
this.executeRecurringNotification,
NOTIFICATION_TIME
);
};
}
playAudioEvery30Seconds = () => {
playAudioEvery30Seconds() {
// Audio alert is disabled dismiss the timer
if (this.audioAlertType === 'none') {
return;
@@ -104,25 +104,25 @@ class DashboardAudioNotificationHelper {
}
this.clearSetTimeout();
};
}
isConversationAssignedToCurrentUser = message => {
isConversationAssignedToCurrentUser(message) {
const conversationAssigneeId = message?.conversation?.assignee_id;
return conversationAssigneeId === this.currentUserId;
};
}
// eslint-disable-next-line class-methods-use-this
isMessageFromCurrentConversation = message => {
isMessageFromCurrentConversation(message) {
return (
window.WOOT_STORE.getters.getSelectedChat?.id === message.conversation_id
);
};
}
isMessageFromCurrentUser = message => {
isMessageFromCurrentUser(message) {
return message?.sender_id === this.currentUserId;
};
}
isUserHasConversationPermission = () => {
isUserHasConversationPermission() {
const currentAccountId = window.WOOT_STORE.getters.getCurrentAccountId;
// Get the user permissions for the current account
const userPermissions = getUserPermissions(
@@ -134,16 +134,16 @@ class DashboardAudioNotificationHelper {
permission => userPermissions.includes(permission)
);
return hasRequiredPermission;
};
}
shouldNotifyOnMessage = message => {
shouldNotifyOnMessage(message) {
if (this.audioAlertType === 'mine') {
return this.isConversationAssignedToCurrentUser(message);
}
return this.audioAlertType === 'all';
};
}
onNewMessage = message => {
onNewMessage(message) {
// If the user does not have the permission to view the conversation, then dismiss the alert
if (!this.isUserHasConversationPermission()) {
return;
@@ -176,7 +176,7 @@ class DashboardAudioNotificationHelper {
window.playAudioAlert();
showBadgeOnFavicon();
this.playAudioEvery30Seconds();
};
}
}
const notifHelper = new DashboardAudioNotificationHelper();
@@ -37,10 +37,8 @@ const storeMock = {
const routerMock = {
currentRoute: {
value: {
name: '',
params: { conversation_id: null },
},
name: '',
params: { conversation_id: null },
},
};
@@ -224,7 +222,7 @@ describe('ReconnectService', () => {
describe('fetchConversationMessagesOnReconnect', () => {
it('should dispatch syncActiveConversationMessages if conversationId exists', async () => {
routerMock.currentRoute.value.params.conversation_id = 1;
routerMock.currentRoute.params.conversation_id = 1;
await reconnectService.fetchConversationMessagesOnReconnect();
expect(storeMock.dispatch).toHaveBeenCalledWith(
'syncActiveConversationMessages',
@@ -233,7 +231,7 @@ describe('ReconnectService', () => {
});
it('should not dispatch syncActiveConversationMessages if conversationId does not exist', async () => {
routerMock.currentRoute.value.params.conversation_id = null;
routerMock.currentRoute.params.conversation_id = null;
await reconnectService.fetchConversationMessagesOnReconnect();
expect(storeMock.dispatch).not.toHaveBeenCalledWith(
'syncActiveConversationMessages',
@@ -307,7 +305,7 @@ describe('ReconnectService', () => {
describe('setConversationLastMessageId', () => {
it('should dispatch setConversationLastMessageId if conversationId exists', async () => {
routerMock.currentRoute.value.params.conversation_id = 1;
routerMock.currentRoute.params.conversation_id = 1;
await reconnectService.setConversationLastMessageId();
expect(storeMock.dispatch).toHaveBeenCalledWith(
'setConversationLastMessageId',
@@ -316,7 +314,7 @@ describe('ReconnectService', () => {
});
it('should not dispatch setConversationLastMessageId if conversationId does not exist', async () => {
routerMock.currentRoute.value.params.conversation_id = null;
routerMock.currentRoute.params.conversation_id = null;
await reconnectService.setConversationLastMessageId();
expect(storeMock.dispatch).not.toHaveBeenCalledWith(
'setConversationLastMessageId',
@@ -0,0 +1,78 @@
import resize from '../../directives/resize';
class ResizeObserverMock {
// eslint-disable-next-line class-methods-use-this
observe() {}
// eslint-disable-next-line class-methods-use-this
unobserve() {}
// eslint-disable-next-line class-methods-use-this
disconnect() {}
}
describe('resize directive', () => {
let el;
let binding;
let observer;
beforeEach(() => {
el = document.createElement('div');
binding = {
value: vi.fn(),
};
observer = {
observe: vi.fn(),
unobserve: vi.fn(),
disconnect: vi.fn(),
};
window.ResizeObserver = ResizeObserverMock;
vi.spyOn(window, 'ResizeObserver').mockImplementation(() => observer);
});
afterEach(() => {
vi.clearAllMocks();
});
it('should create ResizeObserver on bind', () => {
resize.bind(el, binding);
expect(ResizeObserver).toHaveBeenCalled();
expect(observer.observe).toHaveBeenCalledWith(el);
});
it('should call callback on observer callback', () => {
el = document.createElement('div');
binding = {
value: vi.fn(),
};
resize.bind(el, binding);
const entries = [{ contentRect: { width: 100, height: 100 } }];
const callback = binding.value;
callback(entries[0]);
expect(binding.value).toHaveBeenCalledWith(entries[0]);
});
it('should destroy and recreate observer on update', () => {
resize.bind(el, binding);
resize.update(el, { ...binding, oldValue: 'old' });
expect(observer.unobserve).toHaveBeenCalledWith(el);
expect(observer.disconnect).toHaveBeenCalled();
expect(ResizeObserver).toHaveBeenCalledTimes(2);
expect(observer.observe).toHaveBeenCalledTimes(2);
});
it('should destroy observer on unbind', () => {
resize.bind(el, binding);
resize.unbind(el);
expect(observer.unobserve).toHaveBeenCalledWith(el);
expect(observer.disconnect).toHaveBeenCalled();
});
});
@@ -9,8 +9,8 @@ import {
findNodeToInsertImage,
setURLWithQueryAndSize,
} from '../editorHelper';
import { EditorState } from '@chatwoot/prosemirror-schema';
import { EditorView } from '@chatwoot/prosemirror-schema';
import { EditorState } from 'prosemirror-state';
import { EditorView } from 'prosemirror-view';
import { Schema } from 'prosemirror-model';
// Define a basic ProseMirror schema
@@ -65,7 +65,7 @@
"ERROR_MESSAGE": "Couldn't delete the custom attribute. Try again."
},
"CONFIRM": {
"TITLE": "Are you sure want to delete - {attributeName}",
"TITLE": "Are you sure want to delete - %{attributeName}",
"PLACE_HOLDER": "Please type {attributeName} to confirm",
"MESSAGE": "Deleting will remove the custom attribute",
"YES": "Delete ",
@@ -23,52 +23,52 @@
},
"DEFAULT_USER": "System",
"AUTOMATION_RULE": {
"ADD": "{agentName} created a new automation rule (#{id})",
"EDIT": "{agentName} updated an automation rule (#{id})",
"DELETE": "{agentName} deleted an automation rule (#{id})"
"ADD": "%{agentName} created a new automation rule (#%{id})",
"EDIT": "%{agentName} updated an automation rule (#%{id})",
"DELETE": "%{agentName} deleted an automation rule (#%{id})"
},
"ACCOUNT_USER": {
"ADD": "{agentName} invited {invitee} to the account as an {role}",
"ADD": "%{agentName} invited %{invitee} to the account as an %{role}",
"EDIT": {
"SELF": "{agentName} changed their {attributes} to {values}",
"OTHER": "{agentName} changed {attributes} of {user} to {values}",
"DELETED": "{agentName} changed {attributes} of a deleted user to {values}"
"SELF": "%{agentName} changed their %{attributes} to %{values}",
"OTHER": "%{agentName} changed %{attributes} of %{user} to %{values}",
"DELETED": "%{agentName} changed %{attributes} of a deleted user to %{values}"
}
},
"INBOX": {
"ADD": "{agentName} created a new inbox (#{id})",
"EDIT": "{agentName} updated an inbox (#{id})",
"DELETE": "{agentName} deleted an inbox (#{id})"
"ADD": "%{agentName} created a new inbox (#%{id})",
"EDIT": "%{agentName} updated an inbox (#%{id})",
"DELETE": "%{agentName} deleted an inbox (#%{id})"
},
"WEBHOOK": {
"ADD": "{agentName} created a new webhook (#{id})",
"EDIT": "{agentName} updated a webhook (#{id})",
"DELETE": "{agentName} deleted a webhook (#{id})"
"ADD": "%{agentName} created a new webhook (#%{id})",
"EDIT": "%{agentName} updated a webhook (#%{id})",
"DELETE": "%{agentName} deleted a webhook (#%{id})"
},
"USER_ACTION": {
"SIGN_IN": "{agentName} signed in",
"SIGN_OUT": "{agentName} signed out"
"SIGN_IN": "%{agentName} signed in",
"SIGN_OUT": "%{agentName} signed out"
},
"TEAM": {
"ADD": "{agentName} created a new team (#{id})",
"EDIT": "{agentName} updated a team (#{id})",
"DELETE": "{agentName} deleted a team (#{id})"
"ADD": "%{agentName} created a new team (#%{id})",
"EDIT": "%{agentName} updated a team (#%{id})",
"DELETE": "%{agentName} deleted a team (#%{id})"
},
"MACRO": {
"ADD": "{agentName} created a new macro (#{id})",
"EDIT": "{agentName} updated a macro (#{id})",
"DELETE": "{agentName} deleted a macro (#{id})"
"ADD": "%{agentName} created a new macro (#%{id})",
"EDIT": "%{agentName} updated a macro (#%{id})",
"DELETE": "%{agentName} deleted a macro (#%{id})"
},
"INBOX_MEMBER": {
"ADD": "{agentName} added {user} to the inbox(#{inbox_id})",
"REMOVE": "{agentName} removed {user} from the inbox(#{inbox_id})"
"ADD": "%{agentName} added %{user} to the inbox(#%{inbox_id})",
"REMOVE": "%{agentName} removed %{user} from the inbox(#%{inbox_id})"
},
"TEAM_MEMBER": {
"ADD": "{agentName} added {user} to the team(#{team_id})",
"REMOVE": "{agentName} removed {user} from the team(#{team_id})"
"ADD": "%{agentName} added %{user} to the team(#%{team_id})",
"REMOVE": "%{agentName} removed %{user} from the team(#%{team_id})"
},
"ACCOUNT": {
"EDIT": "{agentName} updated the account configuration (#{id})"
"EDIT": "%{agentName} updated the account configuration (#%{id})"
}
}
}
@@ -1,9 +1,9 @@
{
"BULK_ACTION": {
"CONVERSATIONS_SELECTED": "{conversationCount} conversations selected",
"CONVERSATIONS_SELECTED": "%{conversationCount} conversations selected",
"AGENT_SELECT_LABEL": "Select agent",
"ASSIGN_CONFIRMATION_LABEL": "Are you sure to assign {conversationCount} {conversationLabel} to",
"UNASSIGN_CONFIRMATION_LABEL": "Are you sure to unassign {conversationCount} {conversationLabel}?",
"ASSIGN_CONFIRMATION_LABEL": "Are you sure to assign %{conversationCount} %{conversationLabel} to",
"UNASSIGN_CONFIRMATION_LABEL": "Are you sure to unassign %{conversationCount} %{conversationLabel}?",
"GO_BACK_LABEL": "Go back",
"ASSIGN_LABEL": "Assign",
"YES": "Yes",
@@ -367,8 +367,8 @@
},
"SUMMARY": {
"TITLE": "Summary",
"DELETE_WARNING": "Contact of <strong>{primaryContactName}</strong> will be deleted.",
"ATTRIBUTE_WARNING": "Contact details of <strong>{primaryContactName}</strong> will be copied to <strong>{parentContactName}</strong>."
"DELETE_WARNING": "Contact of <strong>%{primaryContactName}</strong> will be deleted.",
"ATTRIBUTE_WARNING": "Contact details of <strong>%{primaryContactName}</strong> will be copied to <strong>%{parentContactName}</strong>."
},
"SEARCH": {
"ERROR": "ERROR_MESSAGE"
@@ -101,7 +101,7 @@
"SELECT_PLACEHOLDER": "None",
"INPUT_PLACEHOLDER": "Select priority",
"NO_RESULTS": "No results found",
"SUCCESSFUL": "Changed priority of conversation id {conversationId} to {priority}",
"SUCCESSFUL": "Changed priority of conversation id %{conversationId} to %{priority}",
"FAILED": "Couldn't change priority. Please try again."
}
},
@@ -122,15 +122,15 @@
"ASSIGN_TEAM": "Assign team",
"API": {
"AGENT_ASSIGNMENT": {
"SUCCESFUL": "Conversation id {conversationId} assigned to \"{agentName}\"",
"SUCCESFUL": "Conversation id %{conversationId} assigned to \"%{agentName}\"",
"FAILED": "Couldn't assign agent. Please try again."
},
"LABEL_ASSIGNMENT": {
"SUCCESFUL": "Assigned label #{labelName} to conversation id {conversationId}",
"SUCCESFUL": "Assigned label #%{labelName} to conversation id %{conversationId}",
"FAILED": "Couldn't assign label. Please try again."
},
"TEAM_ASSIGNMENT": {
"SUCCESFUL": "Assigned team \"{team}\" to conversation id {conversationId}",
"SUCCESFUL": "Assigned team \"%{team}\" to conversation id %{conversationId}",
"FAILED": "Couldn't assign team. Please try again."
}
}
@@ -239,11 +239,11 @@
}
},
"ONBOARDING": {
"TITLE": "Hey 👋, Welcome to {installationName}!",
"DESCRIPTION": "Thanks for signing up. We want you to get the most out of {installationName}. Here are a few things you can do in {installationName} to make the experience delightful.",
"GREETING_MORNING": "👋 Good morning, {name}. Welcome to {installationName}.",
"GREETING_AFTERNOON": "👋 Good afternoon, {name}. Welcome to {installationName}.",
"GREETING_EVENING": "👋 Good evening, {name}. Welcome to {installationName}.",
"TITLE": "Hey 👋, Welcome to %{installationName}!",
"DESCRIPTION": "Thanks for signing up. We want you to get the most out of %{installationName}. Here are a few things you can do in %{installationName} to make the experience delightful.",
"GREETING_MORNING": "👋 Good morning, %{name}. Welcome to %{installationName}.",
"GREETING_AFTERNOON": "👋 Good afternoon, %{name}. Welcome to %{installationName}.",
"GREETING_EVENING": "👋 Good evening, %{name}. Welcome to %{installationName}.",
"READ_LATEST_UPDATES": "Read our latest updates",
"ALL_CONVERSATION": {
"TITLE": "All your conversations in one place",
@@ -317,10 +317,10 @@
"SIDEBAR_TITLE": "Conversation participants",
"NO_RECORDS_FOUND": "No results found",
"ADD_PARTICIPANTS": "Select participants",
"REMANING_PARTICIPANTS_TEXT": "+{count} others",
"REMANING_PARTICIPANT_TEXT": "+{count} other",
"TOTAL_PARTICIPANTS_TEXT": "{count} people are participating.",
"TOTAL_PARTICIPANT_TEXT": "{count} person is participating.",
"REMANING_PARTICIPANTS_TEXT": "+%{count} others",
"REMANING_PARTICIPANT_TEXT": "+%{count} other",
"TOTAL_PARTICIPANTS_TEXT": "%{count} people are participating.",
"TOTAL_PARTICIPANT_TEXT": "%{count} person is participating.",
"NO_PARTICIPANTS_TEXT": "No one is participating!.",
"WATCH_CONVERSATION": "Join conversation",
"YOU_ARE_WATCHING": "You are participating",
@@ -48,7 +48,7 @@
"CUSTOM_EMAIL_DOMAIN_ENABLED": "You can receive emails in your custom domain now."
}
},
"UPDATE_CHATWOOT": "An update {latestChatwootVersion} for Chatwoot is available. Please update your instance.",
"UPDATE_CHATWOOT": "An update %{latestChatwootVersion} for Chatwoot is available. Please update your instance.",
"LEARN_MORE": "Learn more",
"PAYMENT_PENDING": "Your payment is pending. Please update your payment information to continue using Chatwoot",
"LIMITS_UPGRADE": "Your account has exceeded the usage limits, please upgrade your plan to continue using Chatwoot",
@@ -84,7 +84,7 @@
},
"ARTICLE_SEARCH_RESULT": {
"UNCATEGORIZED": "Uncategorized",
"SEARCH_RESULTS": "Search results for {query}",
"SEARCH_RESULTS": "Search results for %{query}",
"EMPTY_TEXT": "Search for articles to insert into replies.",
"SEARCH_LOADER": "Searching...",
"INSERT_ARTICLE": "Insert",
@@ -250,13 +250,13 @@
"DOMAIN": {
"LABEL": "Custom Domain",
"PLACEHOLDER": "Portal custom domain",
"HELP_TEXT": "Add only If you want to use a custom domain for your portals. Eg: {exampleURL}",
"HELP_TEXT": "Add only If you want to use a custom domain for your portals. Eg: %{exampleURL}",
"ERROR": "Enter a valid domain URL"
},
"HOME_PAGE_LINK": {
"LABEL": "Home Page Link",
"PLACEHOLDER": "Portal home page link",
"HELP_TEXT": "The link used to return from the portal to the home page. Eg: {exampleURL}",
"HELP_TEXT": "The link used to return from the portal to the home page. Eg: %{exampleURL}",
"ERROR": "Enter a valid home page URL"
},
"THEME_COLOR": {
@@ -1,7 +1,7 @@
{
"INTEGRATION_APPS": {
"FETCHING": "Fetching Integrations",
"NO_HOOK_CONFIGURED": "There are no {integrationId} integrations configured in this account.",
"NO_HOOK_CONFIGURED": "There are no %{integrationId} integrations configured in this account.",
"HEADER": "Applications",
"STATUS": {
"ENABLED": "Enabled",
@@ -56,7 +56,7 @@
"BUTTON_TEXT": "Disconnect"
},
"SIDEBAR_DESCRIPTION": {
"DIALOGFLOW": "Dialogflow is a natural language understanding platform that makes it easy to design and integrate a conversational user interface into your mobile app, web application, device, bot, interactive voice response system, and so on. <br /> <br /> Dialogflow integration with {installationName} allows you to configure a Dialogflow bot with your inboxes which lets the bot handle the queries initially and hand them over to an agent when needed. Dialogflow can be used to qualifying the leads, reduce the workload of agents by providing frequently asked questions etc. <br /> <br /> To add Dialogflow, you need to create a Service Account in your Google project console and share the credentials. Please refer to the Dialogflow docs for more information."
"DIALOGFLOW": "Dialogflow is a natural language understanding platform that makes it easy to design and integrate a conversational user interface into your mobile app, web application, device, bot, interactive voice response system, and so on. <br /> <br /> Dialogflow integration with %{installationName} allows you to configure a Dialogflow bot with your inboxes which lets the bot handle the queries initially and hand them over to an agent when needed. Dialogflow can be used to qualifying the leads, reduce the workload of agents by providing frequently asked questions etc. <br /> <br /> To add Dialogflow, you need to create a Service Account in your Google project console and share the credentials. Please refer to the Dialogflow docs for more information."
}
}
}
@@ -31,7 +31,7 @@
},
"END_POINT": {
"LABEL": "Webhook URL",
"PLACEHOLDER": "Example: {webhookExampleURL}",
"PLACEHOLDER": "Example: %{webhookExampleURL}",
"ERROR": "Please enter a valid URL"
},
"EDIT_SUBMIT": "Update webhook",
@@ -76,7 +76,7 @@
},
"CONFIRM": {
"TITLE": "Confirm Deletion",
"MESSAGE": "Are you sure to delete the webhook? ({webhookURL})",
"MESSAGE": "Are you sure to delete the webhook? (%{webhookURL})",
"YES": "Yes, Delete ",
"NO": "No, Keep it"
}
@@ -90,7 +90,7 @@
},
"HELP_TEXT": {
"TITLE": "How to use the Slack Integration?",
"BODY": "With this integration, all of your incoming conversations will be synced to the ***{selectedChannelName}*** channel in your Slack workspace. You can manage all your customer conversations right within the channel and never miss a message.\n\nHere are the main features of the integration:\n\n**Respond to conversations from within Slack:** To respond to a conversation in the ***{selectedChannelName}*** Slack channel, simply type out your message and send it as a thread. This will create a response back to the customer through Chatwoot. It's that simple!\n\n **Create private notes:** If you want to create private notes instead of replies, start your message with ***`note:`***. This ensures that your message is kept private and won't be visible to the customer.\n\n**Associate an agent profile:** If the person who replied on Slack has an agent profile in Chatwoot under the same email, the replies will be associated with that agent profile automatically. This means you can easily track who said what and when. On the other hand, when the replier doesn't have an associated agent profile, the replies will appear from the bot profile to the customer.",
"BODY": "With this integration, all of your incoming conversations will be synced to the ***%{selectedChannelName}*** channel in your Slack workspace. You can manage all your customer conversations right within the channel and never miss a message.\n\nHere are the main features of the integration:\n\n**Respond to conversations from within Slack:** To respond to a conversation in the ***%{selectedChannelName}*** Slack channel, simply type out your message and send it as a thread. This will create a response back to the customer through Chatwoot. It's that simple!\n\n **Create private notes:** If you want to create private notes instead of replies, start your message with ***`note:`***. This ensures that your message is kept private and won't be visible to the customer.\n\n**Associate an agent profile:** If the person who replied on Slack has an agent profile in Chatwoot under the same email, the replies will be associated with that agent profile automatically. This means you can easily track who said what and when. On the other hand, when the replier doesn't have an associated agent profile, the replies will appear from the bot profile to the customer.",
"SELECTED": "selected"
},
"SELECT_CHANNEL": {
@@ -114,7 +114,7 @@
},
"OPEN_AI": {
"AI_ASSIST": "AI Assist",
"WITH_AI": " {option} with AI ",
"WITH_AI": " %{option} with AI ",
"OPTIONS": {
"REPLY_SUGGESTION": "Reply Suggestion",
"SUMMARIZE": "Summarize",
@@ -216,7 +216,7 @@
"CONFIRM_YES": "Yes, delete it",
"CONFIRM_NO": "No, keep it",
"TITLE": "Confirm deletion",
"MESSAGE": "Are you sure to delete the app - {appName}?",
"MESSAGE": "Are you sure to delete the app - %{appName}?",
"API_SUCCESS": "Dashboard app deleted successfully",
"API_ERROR": "We couldn't delete the app. Please try again later"
}
@@ -235,7 +235,7 @@
"ERROR": "There was an error fetching the linear issues, please try again",
"LINK_SUCCESS": "Issue linked successfully",
"LINK_ERROR": "There was an error linking the issue, please try again",
"LINK_TITLE": "Conversation (#{conversationId}) with {name}"
"LINK_TITLE": "Conversation (#%{conversationId}) with %{name}"
},
"ADD_OR_LINK": {
"TITLE": "Create/link linear issue",
@@ -294,7 +294,7 @@
"PRIORITY": "Priority",
"ASSIGNEE": "Assignee",
"LABELS": "Labels",
"CREATED_AT": "Created at {createdAt}"
"CREATED_AT": "Created at %{createdAt}"
},
"UNLINK": {
"TITLE": "Unlink",
@@ -23,13 +23,13 @@
"NAME": "First Response Time",
"DESC": "( Avg )",
"INFO_TEXT": "Total number of conversations used for computation:",
"TOOLTIP_TEXT": "First Response Time is {metricValue} (based on {conversationCount} conversations)"
"TOOLTIP_TEXT": "First Response Time is %{metricValue} (based on %{conversationCount} conversations)"
},
"RESOLUTION_TIME": {
"NAME": "Resolution Time",
"DESC": "( Avg )",
"INFO_TEXT": "Total number of conversations used for computation:",
"TOOLTIP_TEXT": "Resolution Time is {metricValue} (based on {conversationCount} conversations)"
"TOOLTIP_TEXT": "Resolution Time is %{metricValue} (based on %{conversationCount} conversations)"
},
"RESOLUTION_COUNT": {
"NAME": "Resolution Count",
@@ -45,7 +45,7 @@
},
"REPLY_TIME": {
"NAME": "Customer waiting time",
"TOOLTIP_TEXT": "Waiting time is {metricValue} (based on {conversationCount} replies)"
"TOOLTIP_TEXT": "Waiting time is %{metricValue} (based on %{conversationCount} replies)"
}
},
"DATE_RANGE_OPTIONS": {
@@ -123,13 +123,13 @@
"NAME": "First Response Time",
"DESC": "( Avg )",
"INFO_TEXT": "Total number of conversations used for computation:",
"TOOLTIP_TEXT": "First Response Time is {metricValue} (based on {conversationCount} conversations)"
"TOOLTIP_TEXT": "First Response Time is %{metricValue} (based on %{conversationCount} conversations)"
},
"RESOLUTION_TIME": {
"NAME": "Resolution Time",
"DESC": "( Avg )",
"INFO_TEXT": "Total number of conversations used for computation:",
"TOOLTIP_TEXT": "Resolution Time is {metricValue} (based on {conversationCount} conversations)"
"TOOLTIP_TEXT": "Resolution Time is %{metricValue} (based on %{conversationCount} conversations)"
},
"RESOLUTION_COUNT": {
"NAME": "Resolution Count",
@@ -190,13 +190,13 @@
"NAME": "First Response Time",
"DESC": "( Avg )",
"INFO_TEXT": "Total number of conversations used for computation:",
"TOOLTIP_TEXT": "First Response Time is {metricValue} (based on {conversationCount} conversations)"
"TOOLTIP_TEXT": "First Response Time is %{metricValue} (based on %{conversationCount} conversations)"
},
"RESOLUTION_TIME": {
"NAME": "Resolution Time",
"DESC": "( Avg )",
"INFO_TEXT": "Total number of conversations used for computation:",
"TOOLTIP_TEXT": "Resolution Time is {metricValue} (based on {conversationCount} conversations)"
"TOOLTIP_TEXT": "Resolution Time is %{metricValue} (based on %{conversationCount} conversations)"
},
"RESOLUTION_COUNT": {
"NAME": "Resolution Count",
@@ -257,13 +257,13 @@
"NAME": "First Response Time",
"DESC": "( Avg )",
"INFO_TEXT": "Total number of conversations used for computation:",
"TOOLTIP_TEXT": "First Response Time is {metricValue} (based on {conversationCount} conversations)"
"TOOLTIP_TEXT": "First Response Time is %{metricValue} (based on %{conversationCount} conversations)"
},
"RESOLUTION_TIME": {
"NAME": "Resolution Time",
"DESC": "( Avg )",
"INFO_TEXT": "Total number of conversations used for computation:",
"TOOLTIP_TEXT": "Resolution Time is {metricValue} (based on {conversationCount} conversations)"
"TOOLTIP_TEXT": "Resolution Time is %{metricValue} (based on %{conversationCount} conversations)"
},
"RESOLUTION_COUNT": {
"NAME": "Resolution Count",
@@ -324,13 +324,13 @@
"NAME": "First Response Time",
"DESC": "( Avg )",
"INFO_TEXT": "Total number of conversations used for computation:",
"TOOLTIP_TEXT": "First Response Time is {metricValue} (based on {conversationCount} conversations)"
"TOOLTIP_TEXT": "First Response Time is %{metricValue} (based on %{conversationCount} conversations)"
},
"RESOLUTION_TIME": {
"NAME": "Resolution Time",
"DESC": "( Avg )",
"INFO_TEXT": "Total number of conversations used for computation:",
"TOOLTIP_TEXT": "Resolution Time is {metricValue} (based on {conversationCount} conversations)"
"TOOLTIP_TEXT": "Resolution Time is %{metricValue} (based on %{conversationCount} conversations)"
},
"RESOLUTION_COUNT": {
"NAME": "Resolution Count",
@@ -436,8 +436,8 @@
"CONVERSATION_HEATMAP": {
"HEADER": "Conversation Traffic",
"NO_CONVERSATIONS": "No conversations",
"CONVERSATION": "{count} conversation",
"CONVERSATIONS": "{count} conversations",
"CONVERSATION": "%{count} conversation",
"CONVERSATIONS": "%{count} conversations",
"DOWNLOAD_REPORT": "Download report"
},
"AGENT_CONVERSATIONS": {
@@ -12,8 +12,8 @@
"MESSAGES": "Messages"
},
"SEARCHING_DATA": "Searching",
"EMPTY_STATE": "No {item} found for query '{query}'",
"EMPTY_STATE_FULL": "No results found for query '{query}'",
"EMPTY_STATE": "No %{item} found for query '%{query}'",
"EMPTY_STATE_FULL": "No results found for query '%{query}'",
"PLACEHOLDER_KEYBINDING": "/ to focus",
"INPUT_PLACEHOLDER": "Type 3 or more characters to search",
"EMPTY_STATE_DEFAULT": "Search by conversation id, email, phone number, messages for better search results. ",
@@ -301,7 +301,7 @@
"TITLE": "Billing",
"CURRENT_PLAN": {
"TITLE": "Current Plan",
"PLAN_NOTE": "You are currently subscribed to the **{plan}** plan with **{quantity}** licenses"
"PLAN_NOTE": "You are currently subscribed to the **%{plan}** plan with **%{quantity}** licenses"
},
"MANAGE_SUBSCRIPTION": {
"TITLE": "Manage your subscription",
@@ -17,7 +17,7 @@
},
"AGENTS": {
"BUTTON_TEXT": "Add agents to team",
"TITLE": "Add agents to team - {teamName}",
"TITLE": "Add agents to team - %{teamName}",
"DESC": "Add Agents to your newly created team. This lets you collaborate as a team on conversations, get notified on new events in the same conversation."
},
"WIZARD": [
@@ -46,7 +46,7 @@
},
"AGENTS": {
"BUTTON_TEXT": "Update agents in team",
"TITLE": "Add agents to team - {teamName}",
"TITLE": "Add agents to team - %{teamName}",
"DESC": "Add Agents to your newly created team. All the added agents will be notified when a conversation is assigned to this team."
},
"WIZARD": [
@@ -77,14 +77,14 @@
"ADD_AGENTS": "Adding Agents to your Team...",
"SELECT": "select",
"SELECT_ALL": "select all agents",
"SELECTED_COUNT": "{selected} out of {total} agents selected."
"SELECTED_COUNT": "%{selected} out of %{total} agents selected."
},
"ADD": {
"TITLE": "Add agents to team - {teamName}",
"TITLE": "Add agents to team - %{teamName}",
"DESC": "Add Agents to your newly created team. This lets you collaborate as a team on conversations, get notified on new events in the same conversation.",
"SELECT": "select",
"SELECT_ALL": "select all agents",
"SELECTED_COUNT": "{selected} out of {total} agents selected.",
"SELECTED_COUNT": "%{selected} out of %{total} agents selected.",
"BUTTON_TEXT": "Add agents",
"AGENT_VALIDATION_ERROR": "Select at least one agent."
},
@@ -3,7 +3,7 @@
"MODAL": {
"TITLE": "Whatsapp Templates",
"SUBTITLE": "Select the whatsapp template you want to send",
"TEMPLATE_SELECTED_SUBTITLE": "Process {templateName}"
"TEMPLATE_SELECTED_SUBTITLE": "Process %{templateName}"
},
"PICKER": {
"SEARCH_PLACEHOLDER": "Search Templates",
@@ -16,7 +16,7 @@
},
"PARSER": {
"VARIABLES_LABEL": "Variables",
"VARIABLE_PLACEHOLDER": "Enter {variable} value",
"VARIABLE_PLACEHOLDER": "Enter %{variable} value",
"GO_BACK_LABEL": "Go Back",
"SEND_MESSAGE_LABEL": "Send Message",
"FORM_ERROR_MESSAGE": "Please fill all variables before sending"
@@ -65,7 +65,7 @@
"ERROR_MESSAGE": "تعذر حذف الصفة المخصصة. حاول مرة أخرى."
},
"CONFIRM": {
"TITLE": "هل أنت متأكد من أنك تريد حذف - {attributeName}",
"TITLE": "هل أنت متأكد من أنك تريد حذف - %{attributeName}",
"PLACE_HOLDER": "الرجاء كتابة {attributeName} للتأكيد",
"MESSAGE": "الحذف سوف يزيل الصفة المخصصة",
"YES": "حذف ",
@@ -23,52 +23,52 @@
},
"DEFAULT_USER": "النظام",
"AUTOMATION_RULE": {
"ADD": "{agentName} أنشأ قاعدة أتمتة جديدة (#{id})",
"EDIT": "{agentName} قام بتحديث قاعدة أتمتة (#{id})",
"DELETE": "{agentName} حذف قاعدة أتمتة (#{id})"
"ADD": "%{agentName} أنشأ قاعدة أتمتة جديدة (#%{id})",
"EDIT": "%{agentName} قام بتحديث قاعدة أتمتة (#%{id})",
"DELETE": "%{agentName} حذف قاعدة أتمتة (#%{id})"
},
"ACCOUNT_USER": {
"ADD": "{agentName} دعا {invitee} إلى الحساب كـ {role}",
"ADD": "%{agentName} دعا %{invitee} إلى الحساب كـ %{role}",
"EDIT": {
"SELF": "{agentName} غير {attributes} الخاصة به إلى {values}",
"OTHER": "{agentName} غير {attributes} لـ {user} إلى {values}",
"DELETED": "{agentName} غير {attributes} لـ {user} إلى {values}"
"SELF": "%{agentName} غير %{attributes} الخاصة به إلى %{values}",
"OTHER": "%{agentName} غير %{attributes} لـ %{user} إلى %{values}",
"DELETED": "%{agentName} غير %{attributes} لـ %{user} إلى %{values}"
}
},
"INBOX": {
"ADD": "{agentName} أنشأ صندوق وارد جديد (#{id})",
"EDIT": "{agentName} قام بتحديث صندوق الوارد (#{id})",
"DELETE": "{agentName} حذف صندوق الوارد (#{id})"
"ADD": "%{agentName} أنشأ صندوق وارد جديد (#%{id})",
"EDIT": "%{agentName} قام بتحديث صندوق الوارد (#%{id})",
"DELETE": "%{agentName} حذف صندوق الوارد (#%{id})"
},
"WEBHOOK": {
"ADD": "{agentName} أنشأ Webhook جديد (#{id})",
"EDIT": "{agentName} قام بتحديث Webhook (#{id})",
"DELETE": "{agentName} حذف Webhook (#{id})"
"ADD": "%{agentName} أنشأ Webhook جديد (#%{id})",
"EDIT": "%{agentName} قام بتحديث Webhook (#%{id})",
"DELETE": "%{agentName} حذف Webhook (#%{id})"
},
"USER_ACTION": {
"SIGN_IN": "{agentName} قام بتسجيل الدخول",
"SIGN_OUT": "{agentName} قام بتسجيل الخروج"
"SIGN_IN": "%{agentName} قام بتسجيل الدخول",
"SIGN_OUT": "%{agentName} قام بتسجيل الخروج"
},
"TEAM": {
"ADD": "{agentName} أنشأ فريق جديد (#{id})",
"EDIT": "{agentName} قام بتحديث الفريق (#{id})",
"DELETE": "{agentName} حذف الفريق (#{id})"
"ADD": "%{agentName} أنشأ فريق جديد (#%{id})",
"EDIT": "%{agentName} قام بتحديث الفريق (#%{id})",
"DELETE": "%{agentName} حذف الفريق (#%{id})"
},
"MACRO": {
"ADD": "{agentName} أنشأ ماكرو جديد (#{id})",
"EDIT": "{agentName} قام بتحديث ماكرو (#{id})",
"DELETE": "{agentName} حذف ماكرو (#{id})"
"ADD": "%{agentName} أنشأ ماكرو جديد (#%{id})",
"EDIT": "%{agentName} قام بتحديث ماكرو (#%{id})",
"DELETE": "%{agentName} حذف ماكرو (#%{id})"
},
"INBOX_MEMBER": {
"ADD": "{agentName} أضاف {user} إلى صندوق الوارد (#{inbox_id})",
"REMOVE": "{agentName} أزال {user} من صندوق الوارد (#{inbox_id})"
"ADD": "%{agentName} أضاف %{user} إلى صندوق الوارد (#%{inbox_id})",
"REMOVE": "%{agentName} أزال %{user} من صندوق الوارد (#%{inbox_id})"
},
"TEAM_MEMBER": {
"ADD": "{agentName} أضاف {user} إلى الفريق (#{team_id})",
"REMOVE": "{agentName} أزال {user} من الفريق (#{team_id})"
"ADD": "%{agentName} أضاف %{user} إلى الفريق (#%{team_id})",
"REMOVE": "%{agentName} أزال %{user} من الفريق (#%{team_id})"
},
"ACCOUNT": {
"EDIT": "{agentName} قام بتحديث إعدادات الحساب (#{id})"
"EDIT": "%{agentName} قام بتحديث إعدادات الحساب (#%{id})"
}
}
}
@@ -1,9 +1,9 @@
{
"BULK_ACTION": {
"CONVERSATIONS_SELECTED": "{conversationCount} المحادثات المحددة",
"CONVERSATIONS_SELECTED": "%{conversationCount} المحادثات المحددة",
"AGENT_SELECT_LABEL": "اختر وكيل",
"ASSIGN_CONFIRMATION_LABEL": "هل أنت متأكد من أنك تريد تعيين {conversationCount} {conversationLabel} إلى",
"UNASSIGN_CONFIRMATION_LABEL": "هل أنت متأكد من إلغاء تعيين {conversationCount} {conversationLabel}؟",
"ASSIGN_CONFIRMATION_LABEL": "هل أنت متأكد من أنك تريد تعيين %{conversationCount} %{conversationLabel} إلى",
"UNASSIGN_CONFIRMATION_LABEL": "هل أنت متأكد من إلغاء تعيين %{conversationCount} %{conversationLabel}؟",
"GO_BACK_LABEL": "العودة للخلف",
"ASSIGN_LABEL": "تكليف",
"YES": "نعم",
@@ -367,8 +367,8 @@
},
"SUMMARY": {
"TITLE": "ملخص",
"DELETE_WARNING": "سيتم حذف جهة الاتصال <strong>{primaryContactName}</strong>.",
"ATTRIBUTE_WARNING": "سيتم نسخ تفاصيل الاتصال من <strong>{primaryContactName}</strong> إلى <strong>{parentContactName}</strong>."
"DELETE_WARNING": "سيتم حذف جهة الاتصال <strong>%{primaryContactName}</strong>.",
"ATTRIBUTE_WARNING": "سيتم نسخ تفاصيل الاتصال من <strong>%{primaryContactName}</strong> إلى <strong>%{parentContactName}</strong>."
},
"SEARCH": {
"ERROR": "رسالة_خطأ"
@@ -101,7 +101,7 @@
"SELECT_PLACEHOLDER": "لا يوجد",
"INPUT_PLACEHOLDER": "تحديد الأولوية",
"NO_RESULTS": "لم يتم العثور على النتائج",
"SUCCESSFUL": "تغيير أولوية معرف المحادثة {conversationId} إلى {priority}",
"SUCCESSFUL": "تغيير أولوية معرف المحادثة %{conversationId} إلى %{priority}",
"FAILED": "تعذر تغيير الأولوية، الرجاء المحاولة مرة أخرى."
}
},
@@ -122,15 +122,15 @@
"ASSIGN_TEAM": "تعيين فريق",
"API": {
"AGENT_ASSIGNMENT": {
"SUCCESFUL": "معرف المحادثة {conversationId} تم تعيينه لـ \"{agentName}\"",
"SUCCESFUL": "معرف المحادثة %{conversationId} تم تعيينه لـ \"%{agentName}\"",
"FAILED": "تعذر تعيين الوكيل. الرجاء المحاولة مرة أخرى."
},
"LABEL_ASSIGNMENT": {
"SUCCESFUL": "تعيين تسمية #{labelName} لمعرف المحادثة {conversationId}",
"SUCCESFUL": "تعيين تسمية #%{labelName} لمعرف المحادثة %{conversationId}",
"FAILED": "تعذر تعيين التسمية. الرجاء المحاولة مرة أخرى."
},
"TEAM_ASSIGNMENT": {
"SUCCESFUL": "الفريق المعين \"{team}\" لمعرف المحادثة {conversationId}",
"SUCCESFUL": "الفريق المعين \"%{team}\" لمعرف المحادثة %{conversationId}",
"FAILED": "تعذر تعيين الفريق. الرجاء المحاولة مرة أخرى."
}
}
@@ -239,11 +239,11 @@
}
},
"ONBOARDING": {
"TITLE": "اهلاً 👋، مرحباً بك في {installationName}!",
"DESCRIPTION": "شكرا للتسجيل. نريدك أن تحصل على أقصى استفادة من {installationName}. إليك بعض الأشياء التي يمكنك القيام بها في {installationName} لجعل التجربة رائعة.",
"GREETING_MORNING": "👋 صباح الخير، {name}. مرحبا بك في {installationName}.",
"GREETING_AFTERNOON": "👋 مساء الخير، {name}. مرحبا بك في {installationName}.",
"GREETING_EVENING": "👋 مساء الخير، {name}. مرحبا بك في {installationName}.",
"TITLE": "اهلاً 👋، مرحباً بك في %{installationName}!",
"DESCRIPTION": "شكرا للتسجيل. نريدك أن تحصل على أقصى استفادة من %{installationName}. إليك بعض الأشياء التي يمكنك القيام بها في %{installationName} لجعل التجربة رائعة.",
"GREETING_MORNING": "👋 صباح الخير، %{name}. مرحبا بك في %{installationName}.",
"GREETING_AFTERNOON": "👋 مساء الخير، %{name}. مرحبا بك في %{installationName}.",
"GREETING_EVENING": "👋 مساء الخير، %{name}. مرحبا بك في %{installationName}.",
"READ_LATEST_UPDATES": "اطلع على آخر التحديثات",
"ALL_CONVERSATION": {
"TITLE": "جميع محادثاتك في مكان واحد",
@@ -317,10 +317,10 @@
"SIDEBAR_TITLE": "المشاركون في المحادثة",
"NO_RECORDS_FOUND": "لم يتم العثور على النتائج",
"ADD_PARTICIPANTS": "اختر المشاركين",
"REMANING_PARTICIPANTS_TEXT": "+{count} أخرى",
"REMANING_PARTICIPANT_TEXT": "+{count} أخرى",
"TOTAL_PARTICIPANTS_TEXT": "{count} شخص مشارك.",
"TOTAL_PARTICIPANT_TEXT": "{count} شخص مشارك.",
"REMANING_PARTICIPANTS_TEXT": "+%{count} أخرى",
"REMANING_PARTICIPANT_TEXT": "+%{count} أخرى",
"TOTAL_PARTICIPANTS_TEXT": "%{count} شخص مشارك.",
"TOTAL_PARTICIPANT_TEXT": "%{count} شخص مشارك.",
"NO_PARTICIPANTS_TEXT": "لا أحد يشارك!",
"WATCH_CONVERSATION": "الانضمام إلى المحادثة",
"YOU_ARE_WATCHING": "أنت مشترك",
@@ -48,7 +48,7 @@
"CUSTOM_EMAIL_DOMAIN_ENABLED": "يمكنك تلقي رسائل البريد الإلكتروني في النطاق المخصص الخاص بك الآن."
}
},
"UPDATE_CHATWOOT": "يتوفر تحديث {latestChatwootVersion} لـ Chatwoot. الرجاء التحديث.",
"UPDATE_CHATWOOT": "يتوفر تحديث %{latestChatwootVersion} لـ Chatwoot. الرجاء التحديث.",
"LEARN_MORE": "اعرف المزيد",
"PAYMENT_PENDING": "الدفعة الخاصة بك معلقة. الرجاء تحديث معلومات الدفع الخاصة بك للاستمرار في استخدام Chatwoot",
"LIMITS_UPGRADE": "لقد تجاوز حسابك حدود الاستخدام، يرجى ترقية خطتك للاستمرار في استخدام Chatwoot",
@@ -84,7 +84,7 @@
},
"ARTICLE_SEARCH_RESULT": {
"UNCATEGORIZED": "Uncategorized",
"SEARCH_RESULTS": "Search results for {query}",
"SEARCH_RESULTS": "Search results for %{query}",
"EMPTY_TEXT": "Search for articles to insert into replies.",
"SEARCH_LOADER": "يبحث...",
"INSERT_ARTICLE": "Insert",
@@ -1,7 +1,7 @@
{
"INTEGRATION_APPS": {
"FETCHING": "جلب التكاملات",
"NO_HOOK_CONFIGURED": "لا يوجد {integrationId} تكاملات مكونة في هذا الحساب.",
"NO_HOOK_CONFIGURED": "لا يوجد %{integrationId} تكاملات مكونة في هذا الحساب.",
"HEADER": "التطبيقات",
"STATUS": {
"ENABLED": "مفعل",
@@ -56,7 +56,7 @@
"BUTTON_TEXT": "قطع الاتصال"
},
"SIDEBAR_DESCRIPTION": {
"DIALOGFLOW": "تدفق الحوار هو منصة لفهم اللغة الطبيعية التي تجعل من السهل تصميم ودمج واجهة المستخدم للمحادثة في تطبيق الهاتف المحمول الخاص بك، تطبيق الويب، الجهاز، البوت، نظام الاستجابة الصوتية التفاعلي، وما إلى ذلك. <br /> <br /> تكامل تدفق الحوار مع {installationName} يسمح لك بتكوين بوت تدفق الحوار مع صناديق الوارد الخاصة بك والذي يتيح للبوت التعامل مع الاستفسارات في البداية وتسليمها إلى وكيل عند الحاجة. ويمكن استخدام تدفق البيانات لتأهيل الخيوط وتقليل عبء العمل الملقى على عاتق الوكلاء عن طريق طرح الأسئلة المتكررة وما إلى ذلك. <br /> <br /> لإضافة تدفق الحوار، تحتاج إلى إنشاء حساب خدمة في وحدة تحكم مشروع جوجل الخاص بك ومشاركة بيانات الاعتماد. يرجى الرجوع إلى مستندات Dialogflow للحصول على مزيد من المعلومات."
"DIALOGFLOW": "تدفق الحوار هو منصة لفهم اللغة الطبيعية التي تجعل من السهل تصميم ودمج واجهة المستخدم للمحادثة في تطبيق الهاتف المحمول الخاص بك، تطبيق الويب، الجهاز، البوت، نظام الاستجابة الصوتية التفاعلي، وما إلى ذلك. <br /> <br /> تكامل تدفق الحوار مع %{installationName} يسمح لك بتكوين بوت تدفق الحوار مع صناديق الوارد الخاصة بك والذي يتيح للبوت التعامل مع الاستفسارات في البداية وتسليمها إلى وكيل عند الحاجة. ويمكن استخدام تدفق البيانات لتأهيل الخيوط وتقليل عبء العمل الملقى على عاتق الوكلاء عن طريق طرح الأسئلة المتكررة وما إلى ذلك. <br /> <br /> لإضافة تدفق الحوار، تحتاج إلى إنشاء حساب خدمة في وحدة تحكم مشروع جوجل الخاص بك ومشاركة بيانات الاعتماد. يرجى الرجوع إلى مستندات Dialogflow للحصول على مزيد من المعلومات."
}
}
}
@@ -76,7 +76,7 @@
},
"CONFIRM": {
"TITLE": "تأكيد الحذف",
"MESSAGE": "هل أنت متأكد من حذف webhook؟ ({webhookURL})",
"MESSAGE": "هل أنت متأكد من حذف webhook؟ (%{webhookURL})",
"YES": "نعم، احذف ",
"NO": "لا، احتفظ به"
}
@@ -114,7 +114,7 @@
},
"OPEN_AI": {
"AI_ASSIST": "AI Assist",
"WITH_AI": " {option} with AI ",
"WITH_AI": " %{option} with AI ",
"OPTIONS": {
"REPLY_SUGGESTION": "Reply Suggestion",
"SUMMARIZE": "Summarize",
@@ -216,7 +216,7 @@
"CONFIRM_YES": "نعم، احذف",
"CONFIRM_NO": "لا، احتفظ به",
"TITLE": "تأكيد الحذف",
"MESSAGE": "هل أنت متأكد من حذف التطبيق - {appName}؟",
"MESSAGE": "هل أنت متأكد من حذف التطبيق - %{appName}؟",
"API_SUCCESS": "تم حذف تطبيق لوحة التحكم بنجاح",
"API_ERROR": "لم نتمكن من حذف التطبيق. الرجاء المحاولة مرة أخرى لاحقاً"
}
@@ -235,7 +235,7 @@
"ERROR": "حدث خطأ أثناء جلب المشكلات من Linear، الرجاء المحاولة مرة أخرى",
"LINK_SUCCESS": "تم ربط المشكلة بنجاح",
"LINK_ERROR": "حدث خطأ أثناء ربط المشكلة، الرجاء المحاولة مرة أخرى",
"LINK_TITLE": "محادثة (#{conversationId}) مع {name}"
"LINK_TITLE": "محادثة (#%{conversationId}) مع %{name}"
},
"ADD_OR_LINK": {
"TITLE": "إنشاء/رابط مشكلة في Linear",
@@ -294,7 +294,7 @@
"PRIORITY": "الأولوية",
"ASSIGNEE": "المكلَّف",
"LABELS": "الوسوم",
"CREATED_AT": "تم إنشاؤها في {createdAt}"
"CREATED_AT": "تم إنشاؤها في %{createdAt}"
},
"UNLINK": {
"TITLE": "إلغاء الربط",
@@ -23,13 +23,13 @@
"NAME": "وقت الاستجابة الأولى",
"DESC": "(متوسط)",
"INFO_TEXT": "العدد الإجمالي للمحادثات المستخدمة في الحساب:",
"TOOLTIP_TEXT": "وقت الرد الأول هو {metricValue} (على أساس {conversationCount} محادثات)"
"TOOLTIP_TEXT": "وقت الرد الأول هو %{metricValue} (على أساس %{conversationCount} محادثات)"
},
"RESOLUTION_TIME": {
"NAME": "وقت إغلاق المحادثات",
"DESC": "(متوسط)",
"INFO_TEXT": "العدد الإجمالي للمحادثات المستخدمة في الحساب:",
"TOOLTIP_TEXT": "وقت الحل هو {metricValue} (على أساس {conversationCount} محادثات)"
"TOOLTIP_TEXT": "وقت الحل هو %{metricValue} (على أساس %{conversationCount} محادثات)"
},
"RESOLUTION_COUNT": {
"NAME": "عدد مرات الإغلاق",
@@ -45,7 +45,7 @@
},
"REPLY_TIME": {
"NAME": "وقت انتظار العميل",
"TOOLTIP_TEXT": "وقت الانتظار هو {metricValue} (بناء على ردود {conversationCount})"
"TOOLTIP_TEXT": "وقت الانتظار هو %{metricValue} (بناء على ردود %{conversationCount})"
}
},
"DATE_RANGE_OPTIONS": {
@@ -167,13 +167,13 @@
"NAME": "وقت الاستجابة الأولى",
"DESC": "(متوسط)",
"INFO_TEXT": "العدد الإجمالي للمحادثات المستخدمة في الحساب:",
"TOOLTIP_TEXT": "وقت الرد الأول هو {metricValue} (على أساس {conversationCount} محادثات)"
"TOOLTIP_TEXT": "وقت الرد الأول هو %{metricValue} (على أساس %{conversationCount} محادثات)"
},
"RESOLUTION_TIME": {
"NAME": "وقت إغلاق المحادثات",
"DESC": "(متوسط)",
"INFO_TEXT": "العدد الإجمالي للمحادثات المستخدمة في الحساب:",
"TOOLTIP_TEXT": "وقت الحل هو {metricValue} (على أساس {conversationCount} محادثات)"
"TOOLTIP_TEXT": "وقت الحل هو %{metricValue} (على أساس %{conversationCount} محادثات)"
},
"RESOLUTION_COUNT": {
"NAME": "عدد مرات الإغلاق",
@@ -234,13 +234,13 @@
"NAME": "وقت الاستجابة الأولى",
"DESC": "(متوسط)",
"INFO_TEXT": "العدد الإجمالي للمحادثات المستخدمة في الحساب:",
"TOOLTIP_TEXT": "وقت الرد الأول هو {metricValue} (على أساس {conversationCount} محادثات)"
"TOOLTIP_TEXT": "وقت الرد الأول هو %{metricValue} (على أساس %{conversationCount} محادثات)"
},
"RESOLUTION_TIME": {
"NAME": "وقت إغلاق المحادثات",
"DESC": "(متوسط)",
"INFO_TEXT": "العدد الإجمالي للمحادثات المستخدمة في الحساب:",
"TOOLTIP_TEXT": "وقت الحل هو {metricValue} (على أساس {conversationCount} محادثات)"
"TOOLTIP_TEXT": "وقت الحل هو %{metricValue} (على أساس %{conversationCount} محادثات)"
},
"RESOLUTION_COUNT": {
"NAME": "عدد مرات الإغلاق",
@@ -301,13 +301,13 @@
"NAME": "وقت الاستجابة الأولى",
"DESC": "(متوسط)",
"INFO_TEXT": "العدد الإجمالي للمحادثات المستخدمة في الحساب:",
"TOOLTIP_TEXT": "وقت الرد الأول هو {metricValue} (على أساس {conversationCount} محادثات)"
"TOOLTIP_TEXT": "وقت الرد الأول هو %{metricValue} (على أساس %{conversationCount} محادثات)"
},
"RESOLUTION_TIME": {
"NAME": "وقت إغلاق المحادثات",
"DESC": "(متوسط)",
"INFO_TEXT": "العدد الإجمالي للمحادثات المستخدمة في الحساب:",
"TOOLTIP_TEXT": "وقت الحل هو {metricValue} (على أساس {conversationCount} محادثات)"
"TOOLTIP_TEXT": "وقت الحل هو %{metricValue} (على أساس %{conversationCount} محادثات)"
},
"RESOLUTION_COUNT": {
"NAME": "عدد مرات الإغلاق",
@@ -368,13 +368,13 @@
"NAME": "وقت الاستجابة الأولى",
"DESC": "(متوسط)",
"INFO_TEXT": "العدد الإجمالي للمحادثات المستخدمة في الحساب:",
"TOOLTIP_TEXT": "وقت الرد الأول هو {metricValue} (على أساس {conversationCount} محادثات)"
"TOOLTIP_TEXT": "وقت الرد الأول هو %{metricValue} (على أساس %{conversationCount} محادثات)"
},
"RESOLUTION_TIME": {
"NAME": "وقت إغلاق المحادثات",
"DESC": "(متوسط)",
"INFO_TEXT": "العدد الإجمالي للمحادثات المستخدمة في الحساب:",
"TOOLTIP_TEXT": "وقت الحل هو {metricValue} (على أساس {conversationCount} محادثات)"
"TOOLTIP_TEXT": "وقت الحل هو %{metricValue} (على أساس %{conversationCount} محادثات)"
},
"RESOLUTION_COUNT": {
"NAME": "عدد مرات الإغلاق",
@@ -480,8 +480,8 @@
"CONVERSATION_HEATMAP": {
"HEADER": "Conversation Traffic",
"NO_CONVERSATIONS": "No conversations",
"CONVERSATION": "{count} conversation",
"CONVERSATIONS": "{count} conversations",
"CONVERSATION": "%{count} conversation",
"CONVERSATIONS": "%{count} conversations",
"DOWNLOAD_REPORT": "Download report"
},
"AGENT_CONVERSATIONS": {
@@ -12,8 +12,8 @@
"MESSAGES": "الرسائل"
},
"SEARCHING_DATA": "جاري البحث",
"EMPTY_STATE": "لم يتم العثور على {item} للطلب '{query}'",
"EMPTY_STATE_FULL": "لم يتم العثور على نتائج للطلب '{query}'",
"EMPTY_STATE": "لم يتم العثور على %{item} للطلب '%{query}'",
"EMPTY_STATE_FULL": "لم يتم العثور على نتائج للطلب '%{query}'",
"PLACEHOLDER_KEYBINDING": "/ للتركيز",
"INPUT_PLACEHOLDER": "أكتب 3 أحرف أو أكثر للبحث",
"EMPTY_STATE_DEFAULT": "البحث عن طريق معرف المحادثة أو البريد الإلكتروني أو رقم الهاتف أو الرسائل للحصول على نتائج بحث أفضل. ",
@@ -301,7 +301,7 @@
"TITLE": "الفواتير",
"CURRENT_PLAN": {
"TITLE": "الباقة الحالية",
"PLAN_NOTE": "أنت مشترك حاليا في باقة**{plan}** مع تراخيص **{quantity}**"
"PLAN_NOTE": "أنت مشترك حاليا في باقة**%{plan}** مع تراخيص **%{quantity}**"
},
"MANAGE_SUBSCRIPTION": {
"TITLE": "إدارة الاشتراك الخاص بك",
@@ -17,7 +17,7 @@
},
"AGENTS": {
"BUTTON_TEXT": "إضافة وكيل للفريق",
"TITLE": "إضافة وكلاء للفريق - {teamName}",
"TITLE": "إضافة وكلاء للفريق - %{teamName}",
"DESC": "إضافة وكلاء إلى فريقك الجديد. هذا يتيح لكم العمل كفريق في المحادثات، والحصول على إشعار عن الأحداث الجديدة في نفس المحادثة."
},
"WIZARD": [
@@ -46,7 +46,7 @@
},
"AGENTS": {
"BUTTON_TEXT": "تحديث الوكلاء في الفريق",
"TITLE": "إضافة وكلاء للفريق - {teamName}",
"TITLE": "إضافة وكلاء للفريق - %{teamName}",
"DESC": "إضافة وكلاء إلى فريقك الذي تم إنشاؤه حديثاً. سيتم إعلام جميع الوكلاء المضافين عند تعيين محادثة لهذا الفريق."
},
"WIZARD": [
@@ -77,14 +77,14 @@
"ADD_AGENTS": "إضافة وكلاء إلى فريقك...",
"SELECT": "حدد",
"SELECT_ALL": "تحديد جميع الوكلاء",
"SELECTED_COUNT": "تم تحديد {selected} من أصل {total} وكيل."
"SELECTED_COUNT": "تم تحديد %{selected} من أصل %{total} وكيل."
},
"ADD": {
"TITLE": "إضافة وكلاء للفريق - {teamName}",
"TITLE": "إضافة وكلاء للفريق - %{teamName}",
"DESC": "إضافة وكلاء إلى فريقك الجديد. هذا يتيح لكم العمل كفريق في المحادثات، والحصول على إشعار عن الأحداث الجديدة في نفس المحادثة.",
"SELECT": "حدد",
"SELECT_ALL": "تحديد جميع الوكلاء",
"SELECTED_COUNT": "تم تحديد {selected} من أصل {total} وكيل.",
"SELECTED_COUNT": "تم تحديد %{selected} من أصل %{total} وكيل.",
"BUTTON_TEXT": "إضافة وكلاء",
"AGENT_VALIDATION_ERROR": "اختر وكيل واحد على الأقل."
},
@@ -3,7 +3,7 @@
"MODAL": {
"TITLE": "قوالب الواتساب",
"SUBTITLE": "حدد القالب الذي تريد إرساله",
"TEMPLATE_SELECTED_SUBTITLE": "معالجة {templateName}"
"TEMPLATE_SELECTED_SUBTITLE": "معالجة %{templateName}"
},
"PICKER": {
"SEARCH_PLACEHOLDER": "نماذج البحث",
@@ -16,7 +16,7 @@
},
"PARSER": {
"VARIABLES_LABEL": "المتغيرات",
"VARIABLE_PLACEHOLDER": "أدخل قيمة {variable}",
"VARIABLE_PLACEHOLDER": "أدخل قيمة %{variable}",
"GO_BACK_LABEL": "العودة للخلف",
"SEND_MESSAGE_LABEL": "إرسال الرسالة",
"FORM_ERROR_MESSAGE": "يرجى ملء جميع المتغيرات قبل الإرسال"
@@ -65,7 +65,7 @@
"ERROR_MESSAGE": "Couldn't delete the custom attribute. Try again."
},
"CONFIRM": {
"TITLE": "Are you sure want to delete - {attributeName}",
"TITLE": "Are you sure want to delete - %{attributeName}",
"PLACE_HOLDER": "Please type {attributeName} to confirm",
"MESSAGE": "Deleting will remove the custom attribute",
"YES": "Изтриване ",
@@ -23,52 +23,52 @@
},
"DEFAULT_USER": "System",
"AUTOMATION_RULE": {
"ADD": "{agentName} created a new automation rule (#{id})",
"EDIT": "{agentName} updated an automation rule (#{id})",
"DELETE": "{agentName} deleted an automation rule (#{id})"
"ADD": "%{agentName} created a new automation rule (#%{id})",
"EDIT": "%{agentName} updated an automation rule (#%{id})",
"DELETE": "%{agentName} deleted an automation rule (#%{id})"
},
"ACCOUNT_USER": {
"ADD": "{agentName} invited {invitee} to the account as an {role}",
"ADD": "%{agentName} invited %{invitee} to the account as an %{role}",
"EDIT": {
"SELF": "{agentName} changed their {attributes} to {values}",
"OTHER": "{agentName} changed {attributes} of {user} to {values}",
"DELETED": "{agentName} changed {attributes} of a deleted user to {values}"
"SELF": "%{agentName} changed their %{attributes} to %{values}",
"OTHER": "%{agentName} changed %{attributes} of %{user} to %{values}",
"DELETED": "%{agentName} changed %{attributes} of a deleted user to %{values}"
}
},
"INBOX": {
"ADD": "{agentName} created a new inbox (#{id})",
"EDIT": "{agentName} updated an inbox (#{id})",
"DELETE": "{agentName} deleted an inbox (#{id})"
"ADD": "%{agentName} created a new inbox (#%{id})",
"EDIT": "%{agentName} updated an inbox (#%{id})",
"DELETE": "%{agentName} deleted an inbox (#%{id})"
},
"WEBHOOK": {
"ADD": "{agentName} created a new webhook (#{id})",
"EDIT": "{agentName} updated a webhook (#{id})",
"DELETE": "{agentName} deleted a webhook (#{id})"
"ADD": "%{agentName} created a new webhook (#%{id})",
"EDIT": "%{agentName} updated a webhook (#%{id})",
"DELETE": "%{agentName} deleted a webhook (#%{id})"
},
"USER_ACTION": {
"SIGN_IN": "{agentName} signed in",
"SIGN_OUT": "{agentName} signed out"
"SIGN_IN": "%{agentName} signed in",
"SIGN_OUT": "%{agentName} signed out"
},
"TEAM": {
"ADD": "{agentName} created a new team (#{id})",
"EDIT": "{agentName} updated a team (#{id})",
"DELETE": "{agentName} deleted a team (#{id})"
"ADD": "%{agentName} created a new team (#%{id})",
"EDIT": "%{agentName} updated a team (#%{id})",
"DELETE": "%{agentName} deleted a team (#%{id})"
},
"MACRO": {
"ADD": "{agentName} created a new macro (#{id})",
"EDIT": "{agentName} updated a macro (#{id})",
"DELETE": "{agentName} deleted a macro (#{id})"
"ADD": "%{agentName} created a new macro (#%{id})",
"EDIT": "%{agentName} updated a macro (#%{id})",
"DELETE": "%{agentName} deleted a macro (#%{id})"
},
"INBOX_MEMBER": {
"ADD": "{agentName} added {user} to the inbox(#{inbox_id})",
"REMOVE": "{agentName} removed {user} from the inbox(#{inbox_id})"
"ADD": "%{agentName} added %{user} to the inbox(#%{inbox_id})",
"REMOVE": "%{agentName} removed %{user} from the inbox(#%{inbox_id})"
},
"TEAM_MEMBER": {
"ADD": "{agentName} added {user} to the team(#{team_id})",
"REMOVE": "{agentName} removed {user} from the team(#{team_id})"
"ADD": "%{agentName} added %{user} to the team(#%{team_id})",
"REMOVE": "%{agentName} removed %{user} from the team(#%{team_id})"
},
"ACCOUNT": {
"EDIT": "{agentName} updated the account configuration (#{id})"
"EDIT": "%{agentName} updated the account configuration (#%{id})"
}
}
}
@@ -1,9 +1,9 @@
{
"BULK_ACTION": {
"CONVERSATIONS_SELECTED": "{conversationCount} conversations selected",
"CONVERSATIONS_SELECTED": "%{conversationCount} conversations selected",
"AGENT_SELECT_LABEL": "Изберете агент",
"ASSIGN_CONFIRMATION_LABEL": "Are you sure to assign {conversationCount} {conversationLabel} to",
"UNASSIGN_CONFIRMATION_LABEL": "Are you sure to unassign {conversationCount} {conversationLabel}?",
"ASSIGN_CONFIRMATION_LABEL": "Are you sure to assign %{conversationCount} %{conversationLabel} to",
"UNASSIGN_CONFIRMATION_LABEL": "Are you sure to unassign %{conversationCount} %{conversationLabel}?",
"GO_BACK_LABEL": "Go back",
"ASSIGN_LABEL": "Assign",
"YES": "Yes",
@@ -367,8 +367,8 @@
},
"SUMMARY": {
"TITLE": "Резюме",
"DELETE_WARNING": "Контакт от <strong>{primaryContactName}</strong> ще бъде изтрит.",
"ATTRIBUTE_WARNING": "Детайлите на контакт <strong>{primaryContactName}</strong> ще бъдат копирани в <strong>{parentContactName}</strong>."
"DELETE_WARNING": "Контакт от <strong>%{primaryContactName}</strong> ще бъде изтрит.",
"ATTRIBUTE_WARNING": "Детайлите на контакт <strong>%{primaryContactName}</strong> ще бъдат копирани в <strong>%{parentContactName}</strong>."
},
"SEARCH": {
"ERROR": "ГРЕШКА"
@@ -101,7 +101,7 @@
"SELECT_PLACEHOLDER": "Нито един",
"INPUT_PLACEHOLDER": "Select priority",
"NO_RESULTS": "Няма намерени резултати",
"SUCCESSFUL": "Changed priority of conversation id {conversationId} to {priority}",
"SUCCESSFUL": "Changed priority of conversation id %{conversationId} to %{priority}",
"FAILED": "Couldn't change priority. Please try again."
}
},
@@ -122,15 +122,15 @@
"ASSIGN_TEAM": "Assign team",
"API": {
"AGENT_ASSIGNMENT": {
"SUCCESFUL": "Conversation id {conversationId} assigned to \"{agentName}\"",
"SUCCESFUL": "Conversation id %{conversationId} assigned to \"%{agentName}\"",
"FAILED": "Couldn't assign agent. Please try again."
},
"LABEL_ASSIGNMENT": {
"SUCCESFUL": "Assigned label #{labelName} to conversation id {conversationId}",
"SUCCESFUL": "Assigned label #%{labelName} to conversation id %{conversationId}",
"FAILED": "Couldn't assign label. Please try again."
},
"TEAM_ASSIGNMENT": {
"SUCCESFUL": "Assigned team \"{team}\" to conversation id {conversationId}",
"SUCCESFUL": "Assigned team \"%{team}\" to conversation id %{conversationId}",
"FAILED": "Couldn't assign team. Please try again."
}
}
@@ -239,11 +239,11 @@
}
},
"ONBOARDING": {
"TITLE": "Hey 👋, Welcome to {installationName}!",
"DESCRIPTION": "Thanks for signing up. We want you to get the most out of {installationName}. Here are a few things you can do in {installationName} to make the experience delightful.",
"GREETING_MORNING": "👋 Good morning, {name}. Welcome to {installationName}.",
"GREETING_AFTERNOON": "👋 Good afternoon, {name}. Welcome to {installationName}.",
"GREETING_EVENING": "👋 Good evening, {name}. Welcome to {installationName}.",
"TITLE": "Hey 👋, Welcome to %{installationName}!",
"DESCRIPTION": "Thanks for signing up. We want you to get the most out of %{installationName}. Here are a few things you can do in %{installationName} to make the experience delightful.",
"GREETING_MORNING": "👋 Good morning, %{name}. Welcome to %{installationName}.",
"GREETING_AFTERNOON": "👋 Good afternoon, %{name}. Welcome to %{installationName}.",
"GREETING_EVENING": "👋 Good evening, %{name}. Welcome to %{installationName}.",
"READ_LATEST_UPDATES": "Read our latest updates",
"ALL_CONVERSATION": {
"TITLE": "All your conversations in one place",
@@ -317,10 +317,10 @@
"SIDEBAR_TITLE": "Conversation participants",
"NO_RECORDS_FOUND": "Няма намерени резултати",
"ADD_PARTICIPANTS": "Select participants",
"REMANING_PARTICIPANTS_TEXT": "+{count} others",
"REMANING_PARTICIPANT_TEXT": "+{count} other",
"TOTAL_PARTICIPANTS_TEXT": "{count} people are participating.",
"TOTAL_PARTICIPANT_TEXT": "{count} person is participating.",
"REMANING_PARTICIPANTS_TEXT": "+%{count} others",
"REMANING_PARTICIPANT_TEXT": "+%{count} other",
"TOTAL_PARTICIPANTS_TEXT": "%{count} people are participating.",
"TOTAL_PARTICIPANT_TEXT": "%{count} person is participating.",
"NO_PARTICIPANTS_TEXT": "No one is participating!.",
"WATCH_CONVERSATION": "Join conversation",
"YOU_ARE_WATCHING": "You are participating",
@@ -48,7 +48,7 @@
"CUSTOM_EMAIL_DOMAIN_ENABLED": "You can receive emails in your custom domain now."
}
},
"UPDATE_CHATWOOT": "An update {latestChatwootVersion} for Chatwoot is available. Please update your instance.",
"UPDATE_CHATWOOT": "An update %{latestChatwootVersion} for Chatwoot is available. Please update your instance.",
"LEARN_MORE": "Learn more",
"PAYMENT_PENDING": "Your payment is pending. Please update your payment information to continue using Chatwoot",
"LIMITS_UPGRADE": "Your account has exceeded the usage limits, please upgrade your plan to continue using Chatwoot",
@@ -84,7 +84,7 @@
},
"ARTICLE_SEARCH_RESULT": {
"UNCATEGORIZED": "Uncategorized",
"SEARCH_RESULTS": "Search results for {query}",
"SEARCH_RESULTS": "Search results for %{query}",
"EMPTY_TEXT": "Search for articles to insert into replies.",
"SEARCH_LOADER": "Searching...",
"INSERT_ARTICLE": "Insert",
@@ -1,7 +1,7 @@
{
"INTEGRATION_APPS": {
"FETCHING": "Fetching Integrations",
"NO_HOOK_CONFIGURED": "There are no {integrationId} integrations configured in this account.",
"NO_HOOK_CONFIGURED": "There are no %{integrationId} integrations configured in this account.",
"HEADER": "Applications",
"STATUS": {
"ENABLED": "Включен",
@@ -56,7 +56,7 @@
"BUTTON_TEXT": "Прекъсване"
},
"SIDEBAR_DESCRIPTION": {
"DIALOGFLOW": "Dialogflow is a natural language understanding platform that makes it easy to design and integrate a conversational user interface into your mobile app, web application, device, bot, interactive voice response system, and so on. <br /> <br /> Dialogflow integration with {installationName} allows you to configure a Dialogflow bot with your inboxes which lets the bot handle the queries initially and hand them over to an agent when needed. Dialogflow can be used to qualifying the leads, reduce the workload of agents by providing frequently asked questions etc. <br /> <br /> To add Dialogflow, you need to create a Service Account in your Google project console and share the credentials. Please refer to the Dialogflow docs for more information."
"DIALOGFLOW": "Dialogflow is a natural language understanding platform that makes it easy to design and integrate a conversational user interface into your mobile app, web application, device, bot, interactive voice response system, and so on. <br /> <br /> Dialogflow integration with %{installationName} allows you to configure a Dialogflow bot with your inboxes which lets the bot handle the queries initially and hand them over to an agent when needed. Dialogflow can be used to qualifying the leads, reduce the workload of agents by providing frequently asked questions etc. <br /> <br /> To add Dialogflow, you need to create a Service Account in your Google project console and share the credentials. Please refer to the Dialogflow docs for more information."
}
}
}
@@ -76,7 +76,7 @@
},
"CONFIRM": {
"TITLE": "Потвърди изтриването",
"MESSAGE": "Are you sure to delete the webhook? ({webhookURL})",
"MESSAGE": "Are you sure to delete the webhook? (%{webhookURL})",
"YES": "Да, изтрий ",
"NO": "No, Keep it"
}
@@ -114,7 +114,7 @@
},
"OPEN_AI": {
"AI_ASSIST": "AI Assist",
"WITH_AI": " {option} with AI ",
"WITH_AI": " %{option} with AI ",
"OPTIONS": {
"REPLY_SUGGESTION": "Reply Suggestion",
"SUMMARIZE": "Summarize",
@@ -216,7 +216,7 @@
"CONFIRM_YES": "Yes, delete it",
"CONFIRM_NO": "No, keep it",
"TITLE": "Потвърди изтриването",
"MESSAGE": "Are you sure to delete the app - {appName}?",
"MESSAGE": "Are you sure to delete the app - %{appName}?",
"API_SUCCESS": "Dashboard app deleted successfully",
"API_ERROR": "We couldn't delete the app. Please try again later"
}
@@ -235,7 +235,7 @@
"ERROR": "There was an error fetching the linear issues, please try again",
"LINK_SUCCESS": "Issue linked successfully",
"LINK_ERROR": "There was an error linking the issue, please try again",
"LINK_TITLE": "Conversation (#{conversationId}) with {name}"
"LINK_TITLE": "Conversation (#%{conversationId}) with %{name}"
},
"ADD_OR_LINK": {
"TITLE": "Create/link linear issue",
@@ -294,7 +294,7 @@
"PRIORITY": "Priority",
"ASSIGNEE": "Assignee",
"LABELS": "Етикети",
"CREATED_AT": "Created at {createdAt}"
"CREATED_AT": "Created at %{createdAt}"
},
"UNLINK": {
"TITLE": "Unlink",
@@ -23,13 +23,13 @@
"NAME": "First Response Time",
"DESC": "( Avg )",
"INFO_TEXT": "Total number of conversations used for computation:",
"TOOLTIP_TEXT": "First Response Time is {metricValue} (based on {conversationCount} conversations)"
"TOOLTIP_TEXT": "First Response Time is %{metricValue} (based on %{conversationCount} conversations)"
},
"RESOLUTION_TIME": {
"NAME": "Resolution Time",
"DESC": "( Avg )",
"INFO_TEXT": "Total number of conversations used for computation:",
"TOOLTIP_TEXT": "Resolution Time is {metricValue} (based on {conversationCount} conversations)"
"TOOLTIP_TEXT": "Resolution Time is %{metricValue} (based on %{conversationCount} conversations)"
},
"RESOLUTION_COUNT": {
"NAME": "Resolution Count",
@@ -45,7 +45,7 @@
},
"REPLY_TIME": {
"NAME": "Customer waiting time",
"TOOLTIP_TEXT": "Waiting time is {metricValue} (based on {conversationCount} replies)"
"TOOLTIP_TEXT": "Waiting time is %{metricValue} (based on %{conversationCount} replies)"
}
},
"DATE_RANGE_OPTIONS": {
@@ -167,13 +167,13 @@
"NAME": "First Response Time",
"DESC": "( Avg )",
"INFO_TEXT": "Total number of conversations used for computation:",
"TOOLTIP_TEXT": "First Response Time is {metricValue} (based on {conversationCount} conversations)"
"TOOLTIP_TEXT": "First Response Time is %{metricValue} (based on %{conversationCount} conversations)"
},
"RESOLUTION_TIME": {
"NAME": "Resolution Time",
"DESC": "( Avg )",
"INFO_TEXT": "Total number of conversations used for computation:",
"TOOLTIP_TEXT": "Resolution Time is {metricValue} (based on {conversationCount} conversations)"
"TOOLTIP_TEXT": "Resolution Time is %{metricValue} (based on %{conversationCount} conversations)"
},
"RESOLUTION_COUNT": {
"NAME": "Resolution Count",
@@ -234,13 +234,13 @@
"NAME": "First Response Time",
"DESC": "( Avg )",
"INFO_TEXT": "Total number of conversations used for computation:",
"TOOLTIP_TEXT": "First Response Time is {metricValue} (based on {conversationCount} conversations)"
"TOOLTIP_TEXT": "First Response Time is %{metricValue} (based on %{conversationCount} conversations)"
},
"RESOLUTION_TIME": {
"NAME": "Resolution Time",
"DESC": "( Avg )",
"INFO_TEXT": "Total number of conversations used for computation:",
"TOOLTIP_TEXT": "Resolution Time is {metricValue} (based on {conversationCount} conversations)"
"TOOLTIP_TEXT": "Resolution Time is %{metricValue} (based on %{conversationCount} conversations)"
},
"RESOLUTION_COUNT": {
"NAME": "Resolution Count",
@@ -301,13 +301,13 @@
"NAME": "First Response Time",
"DESC": "( Avg )",
"INFO_TEXT": "Total number of conversations used for computation:",
"TOOLTIP_TEXT": "First Response Time is {metricValue} (based on {conversationCount} conversations)"
"TOOLTIP_TEXT": "First Response Time is %{metricValue} (based on %{conversationCount} conversations)"
},
"RESOLUTION_TIME": {
"NAME": "Resolution Time",
"DESC": "( Avg )",
"INFO_TEXT": "Total number of conversations used for computation:",
"TOOLTIP_TEXT": "Resolution Time is {metricValue} (based on {conversationCount} conversations)"
"TOOLTIP_TEXT": "Resolution Time is %{metricValue} (based on %{conversationCount} conversations)"
},
"RESOLUTION_COUNT": {
"NAME": "Resolution Count",
@@ -368,13 +368,13 @@
"NAME": "First Response Time",
"DESC": "( Avg )",
"INFO_TEXT": "Total number of conversations used for computation:",
"TOOLTIP_TEXT": "First Response Time is {metricValue} (based on {conversationCount} conversations)"
"TOOLTIP_TEXT": "First Response Time is %{metricValue} (based on %{conversationCount} conversations)"
},
"RESOLUTION_TIME": {
"NAME": "Resolution Time",
"DESC": "( Avg )",
"INFO_TEXT": "Total number of conversations used for computation:",
"TOOLTIP_TEXT": "Resolution Time is {metricValue} (based on {conversationCount} conversations)"
"TOOLTIP_TEXT": "Resolution Time is %{metricValue} (based on %{conversationCount} conversations)"
},
"RESOLUTION_COUNT": {
"NAME": "Resolution Count",
@@ -480,8 +480,8 @@
"CONVERSATION_HEATMAP": {
"HEADER": "Conversation Traffic",
"NO_CONVERSATIONS": "No conversations",
"CONVERSATION": "{count} conversation",
"CONVERSATIONS": "{count} conversations",
"CONVERSATION": "%{count} conversation",
"CONVERSATIONS": "%{count} conversations",
"DOWNLOAD_REPORT": "Download report"
},
"AGENT_CONVERSATIONS": {
@@ -12,8 +12,8 @@
"MESSAGES": "Messages"
},
"SEARCHING_DATA": "Searching",
"EMPTY_STATE": "No {item} found for query '{query}'",
"EMPTY_STATE_FULL": "No results found for query '{query}'",
"EMPTY_STATE": "No %{item} found for query '%{query}'",
"EMPTY_STATE_FULL": "No results found for query '%{query}'",
"PLACEHOLDER_KEYBINDING": "/ to focus",
"INPUT_PLACEHOLDER": "Search messages, contacts or conversations",
"EMPTY_STATE_DEFAULT": "Search by conversation id, email, phone number, messages for better search results.",
@@ -301,7 +301,7 @@
"TITLE": "Billing",
"CURRENT_PLAN": {
"TITLE": "Current Plan",
"PLAN_NOTE": "You are currently subscribed to the **{plan}** plan with **{quantity}** licenses"
"PLAN_NOTE": "You are currently subscribed to the **%{plan}** plan with **%{quantity}** licenses"
},
"MANAGE_SUBSCRIPTION": {
"TITLE": "Manage your subscription",
@@ -17,7 +17,7 @@
},
"AGENTS": {
"BUTTON_TEXT": "Add agents to team",
"TITLE": "Add agents to team - {teamName}",
"TITLE": "Add agents to team - %{teamName}",
"DESC": "Add Agents to your newly created team. This lets you collaborate as a team on conversations, get notified on new events in the same conversation."
},
"WIZARD": [
@@ -46,7 +46,7 @@
},
"AGENTS": {
"BUTTON_TEXT": "Update agents in team",
"TITLE": "Add agents to team - {teamName}",
"TITLE": "Add agents to team - %{teamName}",
"DESC": "Add Agents to your newly created team. All the added agents will be notified when a conversation is assigned to this team."
},
"WIZARD": [
@@ -77,14 +77,14 @@
"ADD_AGENTS": "Adding Agents to your Team...",
"SELECT": "select",
"SELECT_ALL": "select all agents",
"SELECTED_COUNT": "{selected} out of {total} agents selected."
"SELECTED_COUNT": "%{selected} out of %{total} agents selected."
},
"ADD": {
"TITLE": "Add agents to team - {teamName}",
"TITLE": "Add agents to team - %{teamName}",
"DESC": "Add Agents to your newly created team. This lets you collaborate as a team on conversations, get notified on new events in the same conversation.",
"SELECT": "select",
"SELECT_ALL": "select all agents",
"SELECTED_COUNT": "{selected} out of {total} agents selected.",
"SELECTED_COUNT": "%{selected} out of %{total} agents selected.",
"BUTTON_TEXT": "Add agents",
"AGENT_VALIDATION_ERROR": "Select at least one agent."
},
@@ -3,7 +3,7 @@
"MODAL": {
"TITLE": "Whatsapp Templates",
"SUBTITLE": "Select the whatsapp template you want to send",
"TEMPLATE_SELECTED_SUBTITLE": "Process {templateName}"
"TEMPLATE_SELECTED_SUBTITLE": "Process %{templateName}"
},
"PICKER": {
"SEARCH_PLACEHOLDER": "Search Templates",
@@ -16,7 +16,7 @@
},
"PARSER": {
"VARIABLES_LABEL": "Variables",
"VARIABLE_PLACEHOLDER": "Enter {variable} value",
"VARIABLE_PLACEHOLDER": "Enter %{variable} value",
"GO_BACK_LABEL": "Go Back",
"SEND_MESSAGE_LABEL": "Send Message",
"FORM_ERROR_MESSAGE": "Please fill all variables before sending"
@@ -65,7 +65,7 @@
"ERROR_MESSAGE": "No s'ha pogut suprimir l'atribut personalitzat. Torna-ho a provar."
},
"CONFIRM": {
"TITLE": "Estàs segur que vols suprimir - {attributeName}",
"TITLE": "Estàs segur que vols suprimir - %{attributeName}",
"PLACE_HOLDER": "Escriu {attributeName} per confirmar",
"MESSAGE": "En suprimir, s'eliminarà l'atribut personalitzat",
"YES": "Suprimeix ",
@@ -23,52 +23,52 @@
},
"DEFAULT_USER": "Sistema",
"AUTOMATION_RULE": {
"ADD": "{agentName} ha creat una nova regla d'automatització (#{id})",
"EDIT": "{agentName} ha actualitzat una regla d'automatització (#{id})",
"DELETE": "{agentName} ha suprimit una regla d'automatització (#{id})"
"ADD": "%{agentName} ha creat una nova regla d'automatització (#%{id})",
"EDIT": "%{agentName} ha actualitzat una regla d'automatització (#%{id})",
"DELETE": "%{agentName} ha suprimit una regla d'automatització (#%{id})"
},
"ACCOUNT_USER": {
"ADD": "{agentName} ha convidat {invitee} al compte com a {role}",
"ADD": "%{agentName} ha convidat %{invitee} al compte com a %{role}",
"EDIT": {
"SELF": "{agentName} ha canviat els seus {attributes} a {values}",
"OTHER": "{agentName} ha canviat {attributes} de {user} a {values}",
"DELETED": "{agentName} changed {attributes} of a deleted user to {values}"
"SELF": "%{agentName} ha canviat els seus %{attributes} a %{values}",
"OTHER": "%{agentName} ha canviat %{attributes} de %{user} a %{values}",
"DELETED": "%{agentName} changed %{attributes} of a deleted user to %{values}"
}
},
"INBOX": {
"ADD": "{agentName} ha creat una safata d'entrada nova (#{id})",
"EDIT": "{agentName} ha actualitzat una safata d'entrada (#{id})",
"DELETE": "{agentName} ha suprimit una safata d'entrada (#{id})"
"ADD": "%{agentName} ha creat una safata d'entrada nova (#%{id})",
"EDIT": "%{agentName} ha actualitzat una safata d'entrada (#%{id})",
"DELETE": "%{agentName} ha suprimit una safata d'entrada (#%{id})"
},
"WEBHOOK": {
"ADD": "{agentName} ha creat un nou webhook (#{id})",
"EDIT": "{agentName} ha actualitzat un webhook (#{id})",
"DELETE": "{agentName} ha suprimit un webhook (#{id})"
"ADD": "%{agentName} ha creat un nou webhook (#%{id})",
"EDIT": "%{agentName} ha actualitzat un webhook (#%{id})",
"DELETE": "%{agentName} ha suprimit un webhook (#%{id})"
},
"USER_ACTION": {
"SIGN_IN": "{agentName} ha iniciat la sessió",
"SIGN_OUT": "{agentName} ha tancat la sessió"
"SIGN_IN": "%{agentName} ha iniciat la sessió",
"SIGN_OUT": "%{agentName} ha tancat la sessió"
},
"TEAM": {
"ADD": "{agentName} ha creat un equip nou (#{id})",
"EDIT": "{agentName} ha actualitzat un equip (#{id})",
"DELETE": "{agentName} ha suprimit un equip (#{id})"
"ADD": "%{agentName} ha creat un equip nou (#%{id})",
"EDIT": "%{agentName} ha actualitzat un equip (#%{id})",
"DELETE": "%{agentName} ha suprimit un equip (#%{id})"
},
"MACRO": {
"ADD": "{agentName} ha creat una macro nova (#{id})",
"EDIT": "{agentName} ha actualitzat una macro (#{id})",
"DELETE": "{agentName} ha suprimit una macro (#{id})"
"ADD": "%{agentName} ha creat una macro nova (#%{id})",
"EDIT": "%{agentName} ha actualitzat una macro (#%{id})",
"DELETE": "%{agentName} ha suprimit una macro (#%{id})"
},
"INBOX_MEMBER": {
"ADD": "{agentName} ha afegit {user} a la safata d'entrada(#{inbox_id})",
"REMOVE": "{agentName} ha eliminat {user} de la safata d'entrada(#{inbox_id})"
"ADD": "%{agentName} ha afegit %{user} a la safata d'entrada(#%{inbox_id})",
"REMOVE": "%{agentName} ha eliminat %{user} de la safata d'entrada(#%{inbox_id})"
},
"TEAM_MEMBER": {
"ADD": "{agentName} ha afegit {user} a l'equip (#{team_id})",
"REMOVE": "{agentName} ha eliminat {user} de l'equip (#{team_id})"
"ADD": "%{agentName} ha afegit %{user} a l'equip (#%{team_id})",
"REMOVE": "%{agentName} ha eliminat %{user} de l'equip (#%{team_id})"
},
"ACCOUNT": {
"EDIT": "{agentName} ha actualitzat la configuració del compte (#{id})"
"EDIT": "%{agentName} ha actualitzat la configuració del compte (#%{id})"
}
}
}
@@ -1,9 +1,9 @@
{
"BULK_ACTION": {
"CONVERSATIONS_SELECTED": "{conversationCount} converses seleccionades",
"CONVERSATIONS_SELECTED": "%{conversationCount} converses seleccionades",
"AGENT_SELECT_LABEL": "Seleccionar Agent",
"ASSIGN_CONFIRMATION_LABEL": "Estas segur d'assignar {conversationCount} {conversationLabel} a",
"UNASSIGN_CONFIRMATION_LABEL": "Estàs segur de desassignar {conversationCount} {conversationLabel}?",
"ASSIGN_CONFIRMATION_LABEL": "Estas segur d'assignar %{conversationCount} %{conversationLabel} a",
"UNASSIGN_CONFIRMATION_LABEL": "Estàs segur de desassignar %{conversationCount} %{conversationLabel}?",
"GO_BACK_LABEL": "Torna",
"ASSIGN_LABEL": "Assignar",
"YES": "Si",
@@ -367,8 +367,8 @@
},
"SUMMARY": {
"TITLE": "Resum",
"DELETE_WARNING": "El contacte de <strong>{primaryContactName}</strong> es suprimirà.",
"ATTRIBUTE_WARNING": "Les dades de contacte de <strong>{primaryContactName}</strong> es copiaran a <strong>{parentContactName}</strong>."
"DELETE_WARNING": "El contacte de <strong>%{primaryContactName}</strong> es suprimirà.",
"ATTRIBUTE_WARNING": "Les dades de contacte de <strong>%{primaryContactName}</strong> es copiaran a <strong>%{parentContactName}</strong>."
},
"SEARCH": {
"ERROR": "ERROR_MESSAGE"
@@ -101,7 +101,7 @@
"SELECT_PLACEHOLDER": "Ningú",
"INPUT_PLACEHOLDER": "Selecciona la prioritat",
"NO_RESULTS": "No s'ha trobat agents",
"SUCCESSFUL": "S'ha canviat la prioritat de l'identificador de conversa {conversationId} a {priority}",
"SUCCESSFUL": "S'ha canviat la prioritat de l'identificador de conversa %{conversationId} a %{priority}",
"FAILED": "No s'ha pogut canviar la prioritat. Si us plau, torna-ho a provar."
}
},
@@ -122,15 +122,15 @@
"ASSIGN_TEAM": "Assigna un equip",
"API": {
"AGENT_ASSIGNMENT": {
"SUCCESFUL": "Id de conversa {conversationId} assignat a \"{agentName}\"",
"SUCCESFUL": "Id de conversa %{conversationId} assignat a \"%{agentName}\"",
"FAILED": "No s'ha pogut assignar l'agent. Torna-ho a provar."
},
"LABEL_ASSIGNMENT": {
"SUCCESFUL": "S'ha assignat l'etiqueta #{labelName} a l'id de conversa {conversationId}",
"SUCCESFUL": "S'ha assignat l'etiqueta #%{labelName} a l'id de conversa %{conversationId}",
"FAILED": "No s'ha pogut assignar l'etiqueta. Torna-ho a provar."
},
"TEAM_ASSIGNMENT": {
"SUCCESFUL": "S'ha assignat l'equip \"{team}\" a l'id de conversa {conversationId}",
"SUCCESFUL": "S'ha assignat l'equip \"%{team}\" a l'id de conversa %{conversationId}",
"FAILED": "No s'ha pogut assignar l'equip. Torna-ho a provar."
}
}
@@ -239,11 +239,11 @@
}
},
"ONBOARDING": {
"TITLE": "Hola 👋, Benvingut a {installationName}!",
"DESCRIPTION": "Gràcies per registrar-te. Volem que treguis el màxim profit de {installationName}. Aquí teniu algunes coses que pots fer a {installationName} perquè l'experiència sigui agradable.",
"GREETING_MORNING": "👋 Good morning, {name}. Welcome to {installationName}.",
"GREETING_AFTERNOON": "👋 Good afternoon, {name}. Welcome to {installationName}.",
"GREETING_EVENING": "👋 Good evening, {name}. Welcome to {installationName}.",
"TITLE": "Hola 👋, Benvingut a %{installationName}!",
"DESCRIPTION": "Gràcies per registrar-te. Volem que treguis el màxim profit de %{installationName}. Aquí teniu algunes coses que pots fer a %{installationName} perquè l'experiència sigui agradable.",
"GREETING_MORNING": "👋 Good morning, %{name}. Welcome to %{installationName}.",
"GREETING_AFTERNOON": "👋 Good afternoon, %{name}. Welcome to %{installationName}.",
"GREETING_EVENING": "👋 Good evening, %{name}. Welcome to %{installationName}.",
"READ_LATEST_UPDATES": "Llegiu les nostres últimes actualitzacions",
"ALL_CONVERSATION": {
"TITLE": "Totes les teves converses en un sol lloc",
@@ -317,10 +317,10 @@
"SIDEBAR_TITLE": "Participants de la conversa",
"NO_RECORDS_FOUND": "No s'ha trobat agents",
"ADD_PARTICIPANTS": "Selecciona els participants",
"REMANING_PARTICIPANTS_TEXT": "+{count} més",
"REMANING_PARTICIPANT_TEXT": "+{count} més",
"TOTAL_PARTICIPANTS_TEXT": "{count} persones estan participant.",
"TOTAL_PARTICIPANT_TEXT": "{count} persona està participant.",
"REMANING_PARTICIPANTS_TEXT": "+%{count} més",
"REMANING_PARTICIPANT_TEXT": "+%{count} més",
"TOTAL_PARTICIPANTS_TEXT": "%{count} persones estan participant.",
"TOTAL_PARTICIPANT_TEXT": "%{count} persona està participant.",
"NO_PARTICIPANTS_TEXT": "No hi participa ningú!",
"WATCH_CONVERSATION": "Uneix-te a la conversa",
"YOU_ARE_WATCHING": "Estàs participant",
@@ -48,7 +48,7 @@
"CUSTOM_EMAIL_DOMAIN_ENABLED": "Ara podeu rebre correus electrònics al vostre domini personalitzat."
}
},
"UPDATE_CHATWOOT": "L'actualització {latestChatwootVersion} per Chatwoot està disponible. Si us plau, actualitza l'instancia.",
"UPDATE_CHATWOOT": "L'actualització %{latestChatwootVersion} per Chatwoot està disponible. Si us plau, actualitza l'instancia.",
"LEARN_MORE": "Aprèn més",
"PAYMENT_PENDING": "El teu pagament està pendent. Actualitzeu la vostra informació de pagament per continuar utilitzant Chatwoot",
"LIMITS_UPGRADE": "El teu compte ha superat els límits d'ús, actualitza el pla per continuar utilitzant Chatwoot",
@@ -84,7 +84,7 @@
},
"ARTICLE_SEARCH_RESULT": {
"UNCATEGORIZED": "Sense categoria",
"SEARCH_RESULTS": "Resultats de la cerca per a {query}",
"SEARCH_RESULTS": "Resultats de la cerca per a %{query}",
"EMPTY_TEXT": "Cerca articles per inserir a les respostes.",
"SEARCH_LOADER": "S'està cercant...",
"INSERT_ARTICLE": "Insereix",
@@ -1,7 +1,7 @@
{
"INTEGRATION_APPS": {
"FETCHING": "S'estan obtenint integracions",
"NO_HOOK_CONFIGURED": "No hi ha integracions {integrationId} configurades en aquest compte.",
"NO_HOOK_CONFIGURED": "No hi ha integracions %{integrationId} configurades en aquest compte.",
"HEADER": "Aplicacions",
"STATUS": {
"ENABLED": "Habilita",
@@ -56,7 +56,7 @@
"BUTTON_TEXT": "Desconnecta"
},
"SIDEBAR_DESCRIPTION": {
"DIALOGFLOW": "Dialogflow és una plataforma de comprensió del llenguatge natural que facilita el disseny i la integració d'una interfície d'usuari conversacional a la teva aplicació mòbil, aplicació web, dispositiu, bot, sistema interactiu de resposta de veu, etc. <br /> <br /> La integració de Dialogflow amb {installationName} us permet configurar un bot de Dialogflow amb les teves safates d'entrada, la qual cosa permet al bot gestionar les consultes inicialment i lliurar-les a un agent quan sigui necessari. Dialogflow es pot utilitzar per qualificar els clients potencials, reduir la càrrega de treball dels agents proporcionant preguntes freqüents, etc. <br /> <br /> Per afegir Dialogflow, has de crear un compte de servei a la consola del vostre projecte de Google i compartir les credencials. Consulta els documents de Dialogflow per obtenir més informació."
"DIALOGFLOW": "Dialogflow és una plataforma de comprensió del llenguatge natural que facilita el disseny i la integració d'una interfície d'usuari conversacional a la teva aplicació mòbil, aplicació web, dispositiu, bot, sistema interactiu de resposta de veu, etc. <br /> <br /> La integració de Dialogflow amb %{installationName} us permet configurar un bot de Dialogflow amb les teves safates d'entrada, la qual cosa permet al bot gestionar les consultes inicialment i lliurar-les a un agent quan sigui necessari. Dialogflow es pot utilitzar per qualificar els clients potencials, reduir la càrrega de treball dels agents proporcionant preguntes freqüents, etc. <br /> <br /> Per afegir Dialogflow, has de crear un compte de servei a la consola del vostre projecte de Google i compartir les credencials. Consulta els documents de Dialogflow per obtenir més informació."
}
}
}
@@ -76,7 +76,7 @@
},
"CONFIRM": {
"TITLE": "Confirma l'esborrat",
"MESSAGE": "N'estàs segur que suprimiu el webhook? ({webhookURL})",
"MESSAGE": "N'estàs segur que suprimiu el webhook? (%{webhookURL})",
"YES": "Si, esborra ",
"NO": "No, mantén-la"
}
@@ -90,7 +90,7 @@
},
"HELP_TEXT": {
"TITLE": "Com utilitzar la integració de Slack?",
"BODY": "Amb aquesta integració, totes les teves converses entrants se sincronitzaran amb el canal ***{selectedChannelName}*** del teu espai de treball de Slack. Podeu gestionar totes les converses dels vostres clients directament des del canal i no perdreu cap missatge.\n\nAquestes són les principals característiques de la integració:\n\n**Respondre a converses des de Slack:** Per respondre a una conversa al ***{selectedChannelName}*** canal de Slack, només has d'escriure el teu missatge i enviar-lo com a fil. Això crearà una resposta al client mitjançant Chatwoot. Així de senzill!\n\n **Crear notes privades:** si vols crear notes privades en lloc de respostes, comença el teu missatge amb ***`nota:`***. Això garanteix que el vostre missatge es mantingui privat i no serà visible per al client.\n\n**Associar un perfil d'agent:** si la persona que va respondre a Slack té un perfil d'agent a Chatwoot amb el mateix correu electrònic, les respostes s'associaran amb aquest perfil d'agent automàticament. Això vol dir que podeu fer un seguiment fàcilment qui va dir què i quan. D'altra banda, quan el contestador no té un perfil d'agent associat, les respostes apareixeran des del perfil del bot al client.",
"BODY": "Amb aquesta integració, totes les teves converses entrants se sincronitzaran amb el canal ***%{selectedChannelName}*** del teu espai de treball de Slack. Podeu gestionar totes les converses dels vostres clients directament des del canal i no perdreu cap missatge.\n\nAquestes són les principals característiques de la integració:\n\n**Respondre a converses des de Slack:** Per respondre a una conversa al ***%{selectedChannelName}*** canal de Slack, només has d'escriure el teu missatge i enviar-lo com a fil. Això crearà una resposta al client mitjançant Chatwoot. Així de senzill!\n\n **Crear notes privades:** si vols crear notes privades en lloc de respostes, comença el teu missatge amb ***`nota:`***. Això garanteix que el vostre missatge es mantingui privat i no serà visible per al client.\n\n**Associar un perfil d'agent:** si la persona que va respondre a Slack té un perfil d'agent a Chatwoot amb el mateix correu electrònic, les respostes s'associaran amb aquest perfil d'agent automàticament. Això vol dir que podeu fer un seguiment fàcilment qui va dir què i quan. D'altra banda, quan el contestador no té un perfil d'agent associat, les respostes apareixeran des del perfil del bot al client.",
"SELECTED": "seleccionat"
},
"SELECT_CHANNEL": {
@@ -114,7 +114,7 @@
},
"OPEN_AI": {
"AI_ASSIST": "Assistència IA",
"WITH_AI": " {option} amb IA ",
"WITH_AI": " %{option} amb IA ",
"OPTIONS": {
"REPLY_SUGGESTION": "Suggeriment de resposta",
"SUMMARIZE": "Resumir",
@@ -216,7 +216,7 @@
"CONFIRM_YES": "Sí, esborra-ho",
"CONFIRM_NO": "No, mantén-la",
"TITLE": "Confirma la supressió",
"MESSAGE": "N'estàs segur que vols suprimir l'aplicació - {appName}?",
"MESSAGE": "N'estàs segur que vols suprimir l'aplicació - %{appName}?",
"API_SUCCESS": "L'aplicació del tauler de control s'ha esborrat correctament",
"API_ERROR": "No hem pogut esborrar l'aplicació. Intenta-ho més tard"
}
@@ -235,7 +235,7 @@
"ERROR": "S'ha produït un error en obtenir les issues en Linear, torna-ho a provar",
"LINK_SUCCESS": "S'ha enllaçat la issue correctament",
"LINK_ERROR": "S'ha produït un error en enllaçar la issue, torna-ho a provar",
"LINK_TITLE": "Conversa (#{conversationId}) amb {name}"
"LINK_TITLE": "Conversa (#%{conversationId}) amb %{name}"
},
"ADD_OR_LINK": {
"TITLE": "Crear/enllaçar una issue en Linear",
@@ -294,7 +294,7 @@
"PRIORITY": "Prioritat",
"ASSIGNEE": "Cessionari",
"LABELS": "Etiquetes",
"CREATED_AT": "Creat a {createdAt}"
"CREATED_AT": "Creat a %{createdAt}"
},
"UNLINK": {
"TITLE": "Desenllaça",
@@ -23,13 +23,13 @@
"NAME": "Primer Temps de Resposta",
"DESC": "( Promig )",
"INFO_TEXT": "Nombre total de converses utilitzades per al càlcul:",
"TOOLTIP_TEXT": "El temps de primera resposta (FRT) és {metricValue} (basat en {conversationCount} converses)"
"TOOLTIP_TEXT": "El temps de primera resposta (FRT) és %{metricValue} (basat en %{conversationCount} converses)"
},
"RESOLUTION_TIME": {
"NAME": "Temps de resolució",
"DESC": "( Promig )",
"INFO_TEXT": "Nombre total de converses utilitzades per al càlcul:",
"TOOLTIP_TEXT": "El temps de resolució (RT) és {metricValue} (basat en {conversationCount} converses)"
"TOOLTIP_TEXT": "El temps de resolució (RT) és %{metricValue} (basat en %{conversationCount} converses)"
},
"RESOLUTION_COUNT": {
"NAME": "Total de resolucions",
@@ -45,7 +45,7 @@
},
"REPLY_TIME": {
"NAME": "Temps d'espera del client",
"TOOLTIP_TEXT": "El temps d'espera és {metricValue} (basat en {conversationCount} respostes)"
"TOOLTIP_TEXT": "El temps d'espera és %{metricValue} (basat en %{conversationCount} respostes)"
}
},
"DATE_RANGE_OPTIONS": {
@@ -167,13 +167,13 @@
"NAME": "Primer Temps de Resposta",
"DESC": "( Promig )",
"INFO_TEXT": "Nombre total de converses utilitzades per al càlcul:",
"TOOLTIP_TEXT": "El temps de primera resposta (FRT) és {metricValue} (basat en {conversationCount} converses)"
"TOOLTIP_TEXT": "El temps de primera resposta (FRT) és %{metricValue} (basat en %{conversationCount} converses)"
},
"RESOLUTION_TIME": {
"NAME": "Temps de resolució",
"DESC": "( Promig )",
"INFO_TEXT": "Nombre total de converses utilitzades per al càlcul:",
"TOOLTIP_TEXT": "El temps de resolució (RT) és {metricValue} (basat en {conversationCount} converses)"
"TOOLTIP_TEXT": "El temps de resolució (RT) és %{metricValue} (basat en %{conversationCount} converses)"
},
"RESOLUTION_COUNT": {
"NAME": "Total de resolucions",
@@ -234,13 +234,13 @@
"NAME": "Primer Temps de Resposta",
"DESC": "(Mitjana)",
"INFO_TEXT": "Nombre total de converses utilitzades per al càlcul:",
"TOOLTIP_TEXT": "El temps de primera resposta (FRT) és {metricValue} (basat en {conversationCount} converses)"
"TOOLTIP_TEXT": "El temps de primera resposta (FRT) és %{metricValue} (basat en %{conversationCount} converses)"
},
"RESOLUTION_TIME": {
"NAME": "Temps de resolució",
"DESC": "( Promig )",
"INFO_TEXT": "Nombre total de converses utilitzades per al càlcul:",
"TOOLTIP_TEXT": "El temps de resolució (RT) és {metricValue} (basat en {conversationCount} converses)"
"TOOLTIP_TEXT": "El temps de resolució (RT) és %{metricValue} (basat en %{conversationCount} converses)"
},
"RESOLUTION_COUNT": {
"NAME": "Total de resolucions",
@@ -301,13 +301,13 @@
"NAME": "Primer Temps de Resposta",
"DESC": "( Promig )",
"INFO_TEXT": "Nombre total de converses utilitzades per al càlcul:",
"TOOLTIP_TEXT": "El temps de primera resposta (FRT) és {metricValue} (basat en {conversationCount} converses)"
"TOOLTIP_TEXT": "El temps de primera resposta (FRT) és %{metricValue} (basat en %{conversationCount} converses)"
},
"RESOLUTION_TIME": {
"NAME": "Temps de resolució",
"DESC": "( Promig )",
"INFO_TEXT": "Nombre total de converses utilitzades per al càlcul:",
"TOOLTIP_TEXT": "El temps de resolució (RT) és {metricValue} (basat en {conversationCount} converses)"
"TOOLTIP_TEXT": "El temps de resolució (RT) és %{metricValue} (basat en %{conversationCount} converses)"
},
"RESOLUTION_COUNT": {
"NAME": "Total de resolucions",
@@ -368,13 +368,13 @@
"NAME": "Primer Temps de Resposta",
"DESC": "( Promig )",
"INFO_TEXT": "Nombre total de converses utilitzades per al càlcul:",
"TOOLTIP_TEXT": "El temps de primera resposta (FRT) és {metricValue} (basat en {conversationCount} converses)"
"TOOLTIP_TEXT": "El temps de primera resposta (FRT) és %{metricValue} (basat en %{conversationCount} converses)"
},
"RESOLUTION_TIME": {
"NAME": "Temps de resolució",
"DESC": "( Promig )",
"INFO_TEXT": "Nombre total de converses utilitzades per al càlcul:",
"TOOLTIP_TEXT": "El temps de resolució (RT) és {metricValue} (basat en {conversationCount} converses)"
"TOOLTIP_TEXT": "El temps de resolució (RT) és %{metricValue} (basat en %{conversationCount} converses)"
},
"RESOLUTION_COUNT": {
"NAME": "Total de resolucions",
@@ -480,8 +480,8 @@
"CONVERSATION_HEATMAP": {
"HEADER": "Trànsit de conversa",
"NO_CONVERSATIONS": "Sense converses",
"CONVERSATION": "{count} conversa",
"CONVERSATIONS": "{count} converses",
"CONVERSATION": "%{count} conversa",
"CONVERSATIONS": "%{count} converses",
"DOWNLOAD_REPORT": "Download report"
},
"AGENT_CONVERSATIONS": {
@@ -12,8 +12,8 @@
"MESSAGES": "Missatges"
},
"SEARCHING_DATA": "S'està cercant",
"EMPTY_STATE": "No s'ha trobat cap {item} per a la consulta '{query}'",
"EMPTY_STATE_FULL": "No s'han trobat resultats per a la consulta '{query}'",
"EMPTY_STATE": "No s'ha trobat cap %{item} per a la consulta '%{query}'",
"EMPTY_STATE_FULL": "No s'han trobat resultats per a la consulta '%{query}'",
"PLACEHOLDER_KEYBINDING": "/ centrar",
"INPUT_PLACEHOLDER": "Search messages, contacts or conversations",
"EMPTY_STATE_DEFAULT": "Search by conversation id, email, phone number, messages for better search results.",
@@ -301,7 +301,7 @@
"TITLE": "Facturació",
"CURRENT_PLAN": {
"TITLE": "Pla actual",
"PLAN_NOTE": "Actualment estàs subscrit al pla **{plan}** amb **{quantity}** llicències"
"PLAN_NOTE": "Actualment estàs subscrit al pla **%{plan}** amb **%{quantity}** llicències"
},
"MANAGE_SUBSCRIPTION": {
"TITLE": "Gestiona la teva subscripció",
@@ -17,7 +17,7 @@
},
"AGENTS": {
"BUTTON_TEXT": "Afegir agents a l'equip",
"TITLE": "Afegeix agents a l'equip - {teamName}",
"TITLE": "Afegeix agents a l'equip - %{teamName}",
"DESC": "Afegeix agents al teu equip acabat de crear. Això et permet col·laborar com a equip en converses, rebre notificacions sobre esdeveniments nous a la mateixa conversa."
},
"WIZARD": [
@@ -46,7 +46,7 @@
},
"AGENTS": {
"BUTTON_TEXT": "Actualitza els agents de l'equip",
"TITLE": "Afegeix agents a l'equip - {teamName}",
"TITLE": "Afegeix agents a l'equip - %{teamName}",
"DESC": "Afegeix agents al teu equip acabat de crear. Tots els agents afegits rebran una notificació quan s'assigni una conversa a aquest equip."
},
"WIZARD": [
@@ -77,14 +77,14 @@
"ADD_AGENTS": "S'estan afegint agents al teu equip...",
"SELECT": "selecciona",
"SELECT_ALL": "selecciona tots els agents",
"SELECTED_COUNT": "{selected} de {total} agents seleccionats."
"SELECTED_COUNT": "%{selected} de %{total} agents seleccionats."
},
"ADD": {
"TITLE": "Afegeix agents a l'equip - {teamName}",
"TITLE": "Afegeix agents a l'equip - %{teamName}",
"DESC": "Afegeix agents al teu equip acabat de crear. Això et permet col·laborar com a equip en converses, rebre notificacions sobre esdeveniments nous a la mateixa conversa.",
"SELECT": "selecciona",
"SELECT_ALL": "selecciona tots els agents",
"SELECTED_COUNT": "{selected} de {total} agents seleccionats.",
"SELECTED_COUNT": "%{selected} de %{total} agents seleccionats.",
"BUTTON_TEXT": "Afegir agents",
"AGENT_VALIDATION_ERROR": "Selecciona almenys un agent."
},
@@ -3,7 +3,7 @@
"MODAL": {
"TITLE": "Plantilles de Whatsapp",
"SUBTITLE": "Selecciona la plantilla de whatsapp que vols enviar",
"TEMPLATE_SELECTED_SUBTITLE": "Procés {templateName}"
"TEMPLATE_SELECTED_SUBTITLE": "Procés %{templateName}"
},
"PICKER": {
"SEARCH_PLACEHOLDER": "Cerca plantilles",
@@ -16,7 +16,7 @@
},
"PARSER": {
"VARIABLES_LABEL": "Variables",
"VARIABLE_PLACEHOLDER": "Introdueix el valor {variable}",
"VARIABLE_PLACEHOLDER": "Introdueix el valor %{variable}",
"GO_BACK_LABEL": "Torna enrere",
"SEND_MESSAGE_LABEL": "Envia missatge",
"FORM_ERROR_MESSAGE": "Omple totes les variables abans d'enviar-les"
@@ -65,7 +65,7 @@
"ERROR_MESSAGE": "Couldn't delete the custom attribute. Try again."
},
"CONFIRM": {
"TITLE": "Are you sure want to delete - {attributeName}",
"TITLE": "Are you sure want to delete - %{attributeName}",
"PLACE_HOLDER": "Please type {attributeName} to confirm",
"MESSAGE": "Deleting will remove the custom attribute",
"YES": "Vymazat ",
@@ -23,52 +23,52 @@
},
"DEFAULT_USER": "System",
"AUTOMATION_RULE": {
"ADD": "{agentName} created a new automation rule (#{id})",
"EDIT": "{agentName} updated an automation rule (#{id})",
"DELETE": "{agentName} deleted an automation rule (#{id})"
"ADD": "%{agentName} created a new automation rule (#%{id})",
"EDIT": "%{agentName} updated an automation rule (#%{id})",
"DELETE": "%{agentName} deleted an automation rule (#%{id})"
},
"ACCOUNT_USER": {
"ADD": "{agentName} invited {invitee} to the account as an {role}",
"ADD": "%{agentName} invited %{invitee} to the account as an %{role}",
"EDIT": {
"SELF": "{agentName} changed their {attributes} to {values}",
"OTHER": "{agentName} changed {attributes} of {user} to {values}",
"DELETED": "{agentName} changed {attributes} of a deleted user to {values}"
"SELF": "%{agentName} changed their %{attributes} to %{values}",
"OTHER": "%{agentName} changed %{attributes} of %{user} to %{values}",
"DELETED": "%{agentName} changed %{attributes} of a deleted user to %{values}"
}
},
"INBOX": {
"ADD": "{agentName} created a new inbox (#{id})",
"EDIT": "{agentName} updated an inbox (#{id})",
"DELETE": "{agentName} deleted an inbox (#{id})"
"ADD": "%{agentName} created a new inbox (#%{id})",
"EDIT": "%{agentName} updated an inbox (#%{id})",
"DELETE": "%{agentName} deleted an inbox (#%{id})"
},
"WEBHOOK": {
"ADD": "{agentName} created a new webhook (#{id})",
"EDIT": "{agentName} updated a webhook (#{id})",
"DELETE": "{agentName} deleted a webhook (#{id})"
"ADD": "%{agentName} created a new webhook (#%{id})",
"EDIT": "%{agentName} updated a webhook (#%{id})",
"DELETE": "%{agentName} deleted a webhook (#%{id})"
},
"USER_ACTION": {
"SIGN_IN": "{agentName} signed in",
"SIGN_OUT": "{agentName} signed out"
"SIGN_IN": "%{agentName} signed in",
"SIGN_OUT": "%{agentName} signed out"
},
"TEAM": {
"ADD": "{agentName} created a new team (#{id})",
"EDIT": "{agentName} updated a team (#{id})",
"DELETE": "{agentName} deleted a team (#{id})"
"ADD": "%{agentName} created a new team (#%{id})",
"EDIT": "%{agentName} updated a team (#%{id})",
"DELETE": "%{agentName} deleted a team (#%{id})"
},
"MACRO": {
"ADD": "{agentName} created a new macro (#{id})",
"EDIT": "{agentName} updated a macro (#{id})",
"DELETE": "{agentName} deleted a macro (#{id})"
"ADD": "%{agentName} created a new macro (#%{id})",
"EDIT": "%{agentName} updated a macro (#%{id})",
"DELETE": "%{agentName} deleted a macro (#%{id})"
},
"INBOX_MEMBER": {
"ADD": "{agentName} added {user} to the inbox(#{inbox_id})",
"REMOVE": "{agentName} removed {user} from the inbox(#{inbox_id})"
"ADD": "%{agentName} added %{user} to the inbox(#%{inbox_id})",
"REMOVE": "%{agentName} removed %{user} from the inbox(#%{inbox_id})"
},
"TEAM_MEMBER": {
"ADD": "{agentName} added {user} to the team(#{team_id})",
"REMOVE": "{agentName} removed {user} from the team(#{team_id})"
"ADD": "%{agentName} added %{user} to the team(#%{team_id})",
"REMOVE": "%{agentName} removed %{user} from the team(#%{team_id})"
},
"ACCOUNT": {
"EDIT": "{agentName} updated the account configuration (#{id})"
"EDIT": "%{agentName} updated the account configuration (#%{id})"
}
}
}

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