Compare commits

..
Author SHA1 Message Date
Shivam MishraandGitHub c440df9b18 Merge branch 'feat/rollup/2-report-data-source' into feat/rollup/3-rollup-read-path 2026-03-16 19:19:24 +05:30
Shivam MishraandGitHub 68da47753e Merge branch 'feat/rollup/1-model-write-path' into feat/rollup/2-report-data-source 2026-03-16 19:19:14 +05:30
Shivam Mishra d054a60a65 fix: spec flag name 2026-03-16 19:18:49 +05:30
Shivam Mishra 8b7def05dc fix: feature flag 2026-03-16 19:16:51 +05:30
Shivam MishraandGitHub f126230f73 Merge branch 'feat/rollup/2-report-data-source' into feat/rollup/3-rollup-read-path 2026-03-16 19:14:03 +05:30
Shivam MishraandGitHub e42b0be530 Merge branch 'feat/rollup/1-model-write-path' into feat/rollup/2-report-data-source 2026-03-16 19:13:55 +05:30
Shivam Mishra 33b3a56d4c fix: feature flag 2026-03-16 19:06:56 +05:30
Shivam Mishra 6693a4d6f7 fix: rollup 2026-03-16 19:05:45 +05:30
Shivam Mishra 69a44ccf4d refactor: wrap rollup action in a rescue block 2026-03-16 18:53:38 +05:30
Muhsin KelothandGitHub 95727bc608 Merge branch 'develop' into feat/rollup/1-model-write-path 2026-03-16 12:28:41 +04:00
Shivam Mishra 2fab77d7c7 refactor: split large examples in backfill service spec 2026-03-16 12:45:26 +05:30
Shivam Mishra 7ad8011a1b style: fix ABC size 2026-03-16 12:39:52 +05:30
Shivam Mishra c83546e67a style: split specs 2026-03-16 12:37:47 +05:30
Shivam Mishra 1154f9ccb4 chore: remove prompt enable outside resuce 2026-03-16 12:31:45 +05:30
Shivam Mishra e7d5924ca1 feat: make the rollup backfill atomic 2026-03-16 12:26:38 +05:30
Shivam Mishra 89e53e24db fix: timezone offset parsing 2026-03-16 12:04:29 +05:30
Shivam MishraandGitHub 47ecd349cc Merge branch 'develop' into feat/rollup/1-model-write-path 2026-03-16 11:35:49 +05:30
Shivam Mishra b8543c09fb refactor: unify backfill service to per-dimension query pattern
Replace the two-path aggregation (group-then-fan-out for standard events,
per-dimension for distinct-count) with a single per-dimension loop for both.
This removes the bug-prone fan-out logic and the conceptual split.

Strengthen specs to cover all three dimensions (account, agent, inbox),
assert total rollup row counts, and add distinct-count deduplication test.
2026-03-13 15:17:46 +05:30
Shivam MishraandGitHub e37e9fd814 Merge branch 'feat/rollup/2-report-data-source' into feat/rollup/3-rollup-read-path 2026-03-13 13:55:24 +05:30
Shivam MishraandGitHub e6161d2878 Merge branch 'feat/rollup/1-model-write-path' into feat/rollup/2-report-data-source 2026-03-13 13:55:18 +05:30
Shivam Mishra 4e2924538f fix: distinct counts in backfill 2026-03-13 13:48:45 +05:30
Shivam MishraandGitHub e71bda7112 Merge branch 'develop' into feat/rollup/1-model-write-path 2026-03-13 13:18:06 +05:30
Shivam Mishra 31e3e9c1ff chore: add comment 2026-03-13 13:17:49 +05:30
Shivam Mishra 10f1c23742 refactor: use the new registry 2026-03-13 12:46:12 +05:30
Shivam MishraandGitHub 3fd14942f1 Merge branch 'feat/rollup/2-report-data-source' into feat/rollup/3-rollup-read-path 2026-03-13 12:45:20 +05:30
Shivam Mishra 7661104262 refactor: better defaults for metric 2026-03-13 12:44:46 +05:30
Shivam Mishra 38b5402623 refactor: use report metric registry 2026-03-13 12:37:23 +05:30
Shivam MishraandGitHub 3dd29080b5 Merge branch 'feat/rollup/1-model-write-path' into feat/rollup/2-report-data-source 2026-03-13 12:36:23 +05:30
Shivam Mishra e56b4b2539 refactor: metric registry 2026-03-13 12:35:09 +05:30
Shivam MishraandGitHub 5ede10c6df Merge branch 'feat/rollup/1-model-write-path' into feat/rollup/2-report-data-source 2026-03-13 12:29:30 +05:30
Shivam Mishra 8a4c1ec07e chore: add comments 2026-03-13 12:24:21 +05:30
Shivam Mishra ac7a02067e chore: flatten name 2026-03-13 12:15:55 +05:30
Shivam Mishra 2e591afaf6 fix: use IANA 2026-03-13 11:51:28 +05:30
Shivam Mishra 32e923dba0 feat: add rollup data source and route eligible reports through it 2026-03-13 11:02:12 +05:30
Shivam Mishra b824f8ba47 refactor: pass timezone param through report controllers 2026-03-13 10:59:43 +05:30
Shivam Mishra 57d40c373d refactor: route summary builders through DataSource 2026-03-13 10:59:25 +05:30
Shivam Mishra 3c5cd46d2e refactor: replace count and average timeseries builders with unified ReportBuilder 2026-03-13 10:58:57 +05:30
Shivam Mishra d34da0cbf8 feat: extract raw report queries into DataSource abstraction 2026-03-13 10:58:40 +05:30
Shivam Mishra 32b1e16e74 refactor: add timezone_name_from_params to TimezoneHelper 2026-03-13 10:55:32 +05:30
Shivam Mishra 7e14153784 feat: add rake tasks for rollup backfill and timezone setup 2026-03-13 10:55:19 +05:30
Shivam Mishra 914e70568f feat: add backfill service for historical rollup data 2026-03-13 10:55:10 +05:30
Shivam Mishra 13860b9163 feat: write rollups from reporting event listener 2026-03-13 10:54:43 +05:30
Shivam Mishra 42d7f1768c feat: add rollup service to aggregate events in real time 2026-03-13 10:53:37 +05:30
Shivam Mishra 911f211427 feat: add metric registry mapping events to rollup columns 2026-03-13 10:53:29 +05:30
Shivam Mishra ea5779a712 feat: add reporting_timezone setting to account 2026-03-13 10:53:19 +05:30
Shivam Mishra 9ecf5f2504 feat: add reporting_events_rollup table and model 2026-03-13 10:52:57 +05:30
1698 changed files with 42684 additions and 79393 deletions
-1
View File
@@ -1,4 +1,3 @@
---
ignore:
- CVE-2021-41098 # https://github.com/chatwoot/chatwoot/issues/3097 (update once azure blob storage is updated)
- GHSA-57hq-95w6-v4fc # Devise confirmable race condition — patched locally in User model (remove once on Devise 5+)
+2 -2
View File
@@ -166,7 +166,7 @@ GEM
multi_json (~> 1)
statsd-ruby (~> 1.1)
base64 (0.3.0)
bcrypt (3.1.22)
bcrypt (3.1.20)
benchmark (0.4.1)
bigdecimal (3.2.2)
bindex (0.8.1)
@@ -465,7 +465,7 @@ GEM
rails-dom-testing (>= 1, < 3)
railties (>= 4.2.0)
thor (>= 0.14, < 2.0)
json (2.19.2)
json (2.18.1)
json_refs (0.1.8)
hana
json_schemer (0.2.24)
+1 -1
View File
@@ -1 +1 @@
4.12.1
4.11.2
+1
View File
@@ -1,6 +1,7 @@
class V2::ReportBuilder
include DateRangeHelper
include ReportHelper
attr_reader :account, :params
DEFAULT_GROUP_BY = 'day'.freeze
@@ -11,10 +11,6 @@ class V2::Reports::AgentSummaryBuilder < V2::Reports::BaseSummaryBuilder
attr_reader :conversations_count, :resolved_count,
:avg_resolution_time, :avg_first_response_time, :avg_reply_time
def fetch_conversations_count
account.conversations.where(created_at: range).group('assignee_id').count
end
def prepare_report
account.account_users.map do |account_user|
build_agent_stats(account_user)
+29 -32
View File
@@ -1,5 +1,6 @@
class V2::Reports::BaseSummaryBuilder
include DateRangeHelper
include TimezoneHelper
def build
load_data
@@ -9,37 +10,13 @@ class V2::Reports::BaseSummaryBuilder
private
def load_data
@conversations_count = fetch_conversations_count
load_reporting_events_data
end
results = data_source.summary
def load_reporting_events_data
# Extract the column name for indexing (e.g., 'conversations.team_id' -> 'team_id')
index_key = group_by_key.to_s.split('.').last
results = reporting_events
.select(
"#{group_by_key} as #{index_key}",
"COUNT(CASE WHEN name = 'conversation_resolved' THEN 1 END) as resolved_count",
"AVG(CASE WHEN name = 'conversation_resolved' THEN #{average_value_key} END) as avg_resolution_time",
"AVG(CASE WHEN name = 'first_response' THEN #{average_value_key} END) as avg_first_response_time",
"AVG(CASE WHEN name = 'reply_time' THEN #{average_value_key} END) as avg_reply_time"
)
.group(group_by_key)
.index_by { |record| record.public_send(index_key) }
@resolved_count = results.transform_values(&:resolved_count)
@avg_resolution_time = results.transform_values(&:avg_resolution_time)
@avg_first_response_time = results.transform_values(&:avg_first_response_time)
@avg_reply_time = results.transform_values(&:avg_reply_time)
end
def reporting_events
@reporting_events ||= account.reporting_events.where(created_at: range)
end
def fetch_conversations_count
# Override this method
@conversations_count = results.transform_values { |data| data[:conversations_count] }
@resolved_count = results.transform_values { |data| data[:resolved_conversations_count] }
@avg_resolution_time = results.transform_values { |data| data[:avg_resolution_time] }
@avg_first_response_time = results.transform_values { |data| data[:avg_first_response_time] }
@avg_reply_time = results.transform_values { |data| data[:avg_reply_time] }
end
def group_by_key
@@ -50,7 +27,27 @@ class V2::Reports::BaseSummaryBuilder
# Override this method
end
def average_value_key
ActiveModel::Type::Boolean.new.cast(params[:business_hours]).present? ? :value_in_business_hours : :value
def data_source
@data_source ||= Reports::DataSource.for(
account: account,
metric: nil,
dimension_type: summary_dimension_type,
dimension_id: nil,
scope: nil,
range: range,
group_by: 'day',
timezone: timezone_name_from_params(params[:timezone], params[:timezone_offset]),
timezone_offset: params[:timezone_offset],
business_hours: params[:business_hours]
)
end
def summary_dimension_type
{
'account_id' => 'account',
'user_id' => 'agent',
'inbox_id' => 'inbox',
'conversations.team_id' => 'team'
}.fetch(group_by_key.to_s)
end
end
@@ -3,23 +3,10 @@ class V2::Reports::Conversations::BaseReportBuilder
private
AVG_METRICS = %w[avg_first_response_time avg_resolution_time reply_time].freeze
COUNT_METRICS = %w[
conversations_count
incoming_messages_count
outgoing_messages_count
resolutions_count
bot_resolutions_count
bot_handoffs_count
].freeze
def builder_class(metric)
case metric
when *AVG_METRICS
V2::Reports::Timeseries::AverageReportBuilder
when *COUNT_METRICS
V2::Reports::Timeseries::CountReportBuilder
end
return unless Reports::ReportMetricRegistry.supported?(metric)
V2::Reports::Timeseries::ReportBuilder
end
def log_invalid_metric
@@ -11,15 +11,6 @@ class V2::Reports::InboxSummaryBuilder < V2::Reports::BaseSummaryBuilder
attr_reader :conversations_count, :resolved_count,
:avg_resolution_time, :avg_first_response_time, :avg_reply_time
def load_data
@conversations_count = fetch_conversations_count
load_reporting_events_data
end
def fetch_conversations_count
account.conversations.where(created_at: range).group(group_by_key).count
end
def prepare_report
account.inboxes.map do |inbox|
build_inbox_stats(inbox)
@@ -40,8 +31,4 @@ class V2::Reports::InboxSummaryBuilder < V2::Reports::BaseSummaryBuilder
def group_by_key
:inbox_id
end
def average_value_key
ActiveModel::Type::Boolean.new.cast(params[:business_hours]) ? :value_in_business_hours : :value
end
end
@@ -7,8 +7,7 @@ class V2::Reports::LabelSummaryBuilder < V2::Reports::BaseSummaryBuilder
@account = account
@params = params
timezone_offset = (params[:timezone_offset] || 0).to_f
@timezone = ActiveSupport::TimeZone[timezone_offset]&.name
@timezone = timezone_name_from_params(params[:timezone], params[:timezone_offset])
end
# rubocop:enable Lint/MissingSuper
@@ -6,14 +6,6 @@ class V2::Reports::TeamSummaryBuilder < V2::Reports::BaseSummaryBuilder
attr_reader :conversations_count, :resolved_count,
:avg_resolution_time, :avg_first_response_time, :avg_reply_time
def fetch_conversations_count
account.conversations.where(created_at: range).group(:team_id).count
end
def reporting_events
@reporting_events ||= account.reporting_events.where(created_at: range).joins(:conversation)
end
def prepare_report
account.teams.map do |team|
build_team_stats(team)
@@ -1,48 +0,0 @@
class V2::Reports::Timeseries::AverageReportBuilder < V2::Reports::Timeseries::BaseTimeseriesBuilder
def timeseries
grouped_average_time = reporting_events.average(average_value_key)
grouped_event_count = reporting_events.count
grouped_average_time.each_with_object([]) do |element, arr|
event_date, average_time = element
arr << {
value: average_time,
timestamp: event_date.in_time_zone(timezone).to_i,
count: grouped_event_count[event_date]
}
end
end
def aggregate_value
object_scope.average(average_value_key)
end
private
def event_name
metric_to_event_name = {
avg_first_response_time: :first_response,
avg_resolution_time: :conversation_resolved,
reply_time: :reply_time
}
metric_to_event_name[params[:metric].to_sym]
end
def object_scope
scope.reporting_events.where(name: event_name, created_at: range, account_id: account.id)
end
def reporting_events
@grouped_values = object_scope.group_by_period(
group_by,
:created_at,
default_value: 0,
range: range,
permit: %w[day week month year hour],
time_zone: timezone
)
end
def average_value_key
@average_value_key ||= params[:business_hours].present? ? :value_in_business_hours : :value
end
end
@@ -1,12 +1,13 @@
class V2::Reports::Timeseries::BaseTimeseriesBuilder
include TimezoneHelper
include DateRangeHelper
DEFAULT_GROUP_BY = 'day'.freeze
pattr_initialize :account, :params
def scope
case params[:type].to_sym
case dimension_type.to_sym
when :account
account
when :inbox
@@ -20,6 +21,21 @@ class V2::Reports::Timeseries::BaseTimeseriesBuilder
end
end
def data_source
@data_source ||= Reports::DataSource.for(
account: account,
metric: params[:metric],
dimension_type: dimension_type,
dimension_id: params[:id],
scope: scope,
range: range,
group_by: group_by,
timezone: timezone,
timezone_offset: params[:timezone_offset],
business_hours: params[:business_hours]
)
end
def inbox
@inbox ||= account.inboxes.find(params[:id])
end
@@ -41,6 +57,12 @@ class V2::Reports::Timeseries::BaseTimeseriesBuilder
end
def timezone
@timezone ||= timezone_name_from_offset(params[:timezone_offset])
@timezone ||= timezone_name_from_params(params[:timezone], params[:timezone_offset])
end
private
def dimension_type
(params[:type].presence || 'account').to_s
end
end
@@ -1,78 +0,0 @@
class V2::Reports::Timeseries::CountReportBuilder < V2::Reports::Timeseries::BaseTimeseriesBuilder
def timeseries
grouped_count.each_with_object([]) do |element, arr|
event_date, event_count = element
# The `event_date` is in Date format (without time), such as "Wed, 15 May 2024".
# We need a timestamp for the start of the day. However, we can't use `event_date.to_time.to_i`
# because it converts the date to 12:00 AM server timezone.
# The desired output should be 12:00 AM in the specified timezone.
arr << { value: event_count, timestamp: event_date.in_time_zone(timezone).to_i }
end
end
def aggregate_value
object_scope.count
end
private
def metric
@metric ||= params[:metric]
end
def object_scope
send("scope_for_#{metric}")
end
def scope_for_conversations_count
scope.conversations.where(account_id: account.id, created_at: range)
end
def scope_for_incoming_messages_count
scope.messages.where(account_id: account.id, created_at: range).incoming.unscope(:order)
end
def scope_for_outgoing_messages_count
scope.messages.where(account_id: account.id, created_at: range).outgoing.unscope(:order)
end
def scope_for_resolutions_count
scope.reporting_events.where(
name: :conversation_resolved,
account_id: account.id,
created_at: range
)
end
def scope_for_bot_resolutions_count
scope.reporting_events.where(
name: :conversation_bot_resolved,
account_id: account.id,
created_at: range
)
end
def scope_for_bot_handoffs_count
scope.reporting_events.joins(:conversation).select(:conversation_id).where(
name: :conversation_bot_handoff,
account_id: account.id,
created_at: range
).distinct
end
def grouped_count
# IMPORTANT: time_zone parameter affects both data grouping AND output timestamps
# It converts timestamps to the target timezone before grouping, which means
# the same event can fall into different day buckets depending on timezone
# Example: 2024-01-15 00:00 UTC becomes 2024-01-14 16:00 PST (falls on different day)
@grouped_values = object_scope.group_by_period(
group_by,
:created_at,
default_value: 0,
range: range,
permit: %w[day week month year hour],
time_zone: timezone
).count
end
end
@@ -0,0 +1,9 @@
class V2::Reports::Timeseries::ReportBuilder < V2::Reports::Timeseries::BaseTimeseriesBuilder
def timeseries
data_source.timeseries
end
def aggregate_value
data_source.aggregate
end
end
@@ -126,7 +126,7 @@ class Api::V1::Accounts::Integrations::LinearController < Api::V1::Accounts::Bas
return unless @hook&.access_token
begin
linear_client = Linear.new(@hook.access_token, refresh_token: @hook.settings&.[]('refresh_token'))
linear_client = Linear.new(@hook.access_token)
linear_client.revoke_token
rescue StandardError => e
Rails.logger.error "Failed to revoke Linear token: #{e.message}"
@@ -79,7 +79,7 @@ class Api::V1::Accounts::PortalsController < Api::V1::Accounts::BaseController
def portal_params
params.require(:portal).permit(
:id, :color, :custom_domain, :header_text, :homepage_link,
:name, :page_title, :slug, :archived, { config: [:default_locale, { allowed_locales: [] }, { draft_locales: [] }] }
:name, :page_title, :slug, :archived, { config: [:default_locale, { allowed_locales: [] }] }
)
end
@@ -112,6 +112,7 @@ class Api::V2::Accounts::ReportsController < Api::V1::Accounts::BaseController
common_params.merge({
since: range[:current][:since],
until: range[:current][:until],
timezone: params[:timezone],
timezone_offset: params[:timezone_offset]
})
end
@@ -120,6 +121,7 @@ class Api::V2::Accounts::ReportsController < Api::V1::Accounts::BaseController
common_params.merge({
since: range[:previous][:since],
until: range[:previous][:until],
timezone: params[:timezone],
timezone_offset: params[:timezone_offset]
})
end
@@ -129,6 +131,7 @@ class Api::V2::Accounts::ReportsController < Api::V1::Accounts::BaseController
metric: params[:metric],
since: params[:since],
until: params[:until],
timezone: params[:timezone],
timezone_offset: params[:timezone_offset]
})
end
@@ -3,15 +3,15 @@ class Api::V2::Accounts::SummaryReportsController < Api::V1::Accounts::BaseContr
before_action :prepare_builder_params, only: [:agent, :team, :inbox, :label, :channel]
def agent
render_report_with(V2::Reports::AgentSummaryBuilder)
render_report_with(V2::Reports::AgentSummaryBuilder, type: :agent)
end
def team
render_report_with(V2::Reports::TeamSummaryBuilder)
render_report_with(V2::Reports::TeamSummaryBuilder, type: :team)
end
def inbox
render_report_with(V2::Reports::InboxSummaryBuilder)
render_report_with(V2::Reports::InboxSummaryBuilder, type: :inbox)
end
def label
@@ -36,15 +36,18 @@ class Api::V2::Accounts::SummaryReportsController < Api::V1::Accounts::BaseContr
until: permitted_params[:until],
business_hours: ActiveModel::Type::Boolean.new.cast(permitted_params[:business_hours])
}
@builder_params[:timezone] = permitted_params[:timezone] if permitted_params[:timezone].present?
@builder_params[:timezone_offset] = permitted_params[:timezone_offset] if permitted_params[:timezone_offset].present?
end
def render_report_with(builder_class)
builder = builder_class.new(account: Current.account, params: @builder_params)
def render_report_with(builder_class, type: nil)
builder_params = type.present? ? @builder_params.merge(type: type) : @builder_params
builder = builder_class.new(account: Current.account, params: builder_params)
render json: builder.build
end
def permitted_params
params.permit(:since, :until, :business_hours)
params.permit(:since, :until, :business_hours, :timezone, :timezone_offset)
end
def date_range_too_long?
+12 -39
View File
@@ -2,8 +2,6 @@ class Linear::CallbacksController < ApplicationController
include Linear::IntegrationHelper
def show
return redirect_to(safe_linear_redirect_uri) if params[:code].blank? || account_id.blank?
@response = oauth_client.auth_code.get_token(
params[:code],
redirect_uri: "#{base_url}/linear/callback"
@@ -12,7 +10,7 @@ class Linear::CallbacksController < ApplicationController
handle_response
rescue StandardError => e
Rails.logger.error("Linear callback error: #{e.message}")
redirect_to safe_linear_redirect_uri
redirect_to linear_redirect_uri
end
private
@@ -33,19 +31,22 @@ class Linear::CallbacksController < ApplicationController
end
def handle_response
raise ArgumentError, 'Missing access token in Linear OAuth response' if parsed_body['access_token'].blank?
hook = account.hooks.find_or_initialize_by(app_id: 'linear')
hook.assign_attributes(
hook = account.hooks.new(
access_token: parsed_body['access_token'],
status: 'enabled',
settings: merged_integration_settings(hook.settings)
app_id: 'linear',
settings: {
token_type: parsed_body['token_type'],
expires_in: parsed_body['expires_in'],
scope: parsed_body['scope']
}
)
# You may wonder why we're not handling the refresh token update, since the token will expire only after 10 years, https://github.com/linear/linear/issues/251
hook.save!
redirect_to linear_redirect_uri
rescue StandardError => e
Rails.logger.error("Linear callback error: #{e.message}")
redirect_to safe_linear_redirect_uri
redirect_to linear_redirect_uri
end
def account
@@ -53,47 +54,19 @@ class Linear::CallbacksController < ApplicationController
end
def account_id
return @account_id if instance_variable_defined?(:@account_id)
return unless params[:state]
@account_id = params[:state].present? ? verify_linear_token(params[:state]) : nil
verify_linear_token(params[:state])
end
def linear_redirect_uri
"#{ENV.fetch('FRONTEND_URL', nil)}/app/accounts/#{account.id}/settings/integrations/linear"
end
def safe_linear_redirect_uri
return base_url if account_id.blank?
linear_redirect_uri
rescue StandardError
base_url
end
def parsed_body
@parsed_body ||= @response.response.parsed
end
def integration_settings
{
token_type: parsed_body['token_type'],
expires_in: parsed_body['expires_in'],
expires_on: expires_on,
scope: parsed_body['scope'],
refresh_token: parsed_body['refresh_token']
}.compact
end
def merged_integration_settings(existing_settings)
existing_settings.to_h.with_indifferent_access.merge(integration_settings)
end
def expires_on
return if parsed_body['expires_in'].blank?
(Time.current.utc + parsed_body['expires_in'].to_i.seconds).to_s
end
def base_url
ENV.fetch('FRONTEND_URL', 'http://localhost:3000')
end
@@ -1,101 +0,0 @@
class Platform::Api::V1::EmailChannelMigrationsController < PlatformController
before_action :set_account
before_action :validate_account_permissible
before_action :validate_feature_flag
before_action :validate_params
def create
results = migrate_email_channels
render json: { results: results }, status: :ok
end
private
def set_account
@account = Account.find(params[:account_id])
end
def validate_account_permissible
return if @platform_app.platform_app_permissibles.find_by(permissible: @account)
render json: { error: 'Non permissible resource' }, status: :unauthorized
end
def validate_feature_flag
return if ActiveModel::Type::Boolean.new.cast(ENV.fetch('EMAIL_CHANNEL_MIGRATION', false))
render json: { error: 'Email channel migration is not enabled' }, status: :forbidden
end
def validate_params
return render json: { error: 'Missing migrations parameter' }, status: :unprocessable_entity if migration_params.blank?
return unless migration_params.size > MAX_MIGRATIONS
return render json: { error: "Too many migrations (max #{MAX_MIGRATIONS})" },
status: :unprocessable_entity
end
def migrate_email_channels
migration_params.map { |entry| migrate_single(entry) }
end
MAX_MIGRATIONS = 25
SUPPORTED_PROVIDERS = %w[google microsoft].freeze
def migrate_single(entry)
validate_provider!(entry[:provider])
ActiveRecord::Base.transaction do
channel = create_channel(entry)
inbox = create_inbox(channel, entry)
{ email: entry[:email], inbox_id: inbox.id, channel_id: channel.id, status: 'success' }
end
rescue StandardError => e
{ email: entry[:email], status: 'error', message: e.message }
end
def create_channel(entry)
Channel::Email.create!(
account_id: @account.id,
email: entry[:email],
provider: entry[:provider],
provider_config: entry[:provider_config]&.to_h,
imap_enabled: entry.fetch(:imap_enabled, true),
imap_address: entry[:imap_address] || default_imap_address(entry[:provider]),
imap_port: entry[:imap_port] || 993,
imap_login: entry[:imap_login] || entry[:email],
imap_enable_ssl: entry.fetch(:imap_enable_ssl, true)
)
end
def create_inbox(channel, entry)
@account.inboxes.create!(
name: entry[:inbox_name] || "Migrated #{entry[:provider]&.capitalize}: #{entry[:email]}",
channel: channel
)
end
def validate_provider!(provider)
return if SUPPORTED_PROVIDERS.include?(provider)
raise ArgumentError, "Unsupported provider '#{provider}'. Must be one of: #{SUPPORTED_PROVIDERS.join(', ')}"
end
def default_imap_address(provider)
case provider
when 'google' then 'imap.gmail.com'
when 'microsoft' then 'outlook.office365.com'
else ''
end
end
def migration_params
params.permit(migrations: [
:email, :provider, :inbox_name,
:imap_enabled, :imap_address, :imap_port, :imap_login, :imap_enable_ssl,
{ provider_config: {} }
])[:migrations]
end
end
@@ -6,7 +6,6 @@ class Public::Api::V1::Portals::ArticlesController < Public::Api::V1::Portals::B
layout 'portal'
def index
@search_query = list_params[:query]
@articles = @portal.articles.published.includes(:category, :author)
@articles = @articles.where(locale: permitted_params[:locale]) if permitted_params[:locale].present?
@@ -74,9 +73,7 @@ class Public::Api::V1::Portals::ArticlesController < Public::Api::V1::Portals::B
end
def list_params
@list_params ||= params.permit(:query, :locale, :sort, :status, :page, :per_page).tap do |permitted|
permitted[:query] = permitted[:query].to_s.strip.presence
end
params.permit(:query, :locale, :sort, :status, :page, :per_page)
end
def permitted_params
+1 -11
View File
@@ -77,23 +77,13 @@ class WidgetsController < ActionController::Base
end
def allow_iframe_requests
if @web_widget.allowed_domains.blank? || embedded_from_non_web_origin?
if @web_widget.allowed_domains.blank?
response.headers.delete('X-Frame-Options')
else
domains = @web_widget.allowed_domains.split(',').map(&:strip).join(' ')
response.headers['Content-Security-Policy'] = "frame-ancestors #{domains}"
end
end
# Mobile WebViews (iOS/Android) load content from file:// or null origins,
# which cannot match any domain in frame-ancestors. When the per-inbox flag
# is enabled, skip frame-ancestors for these requests.
def embedded_from_non_web_origin?
return false unless @web_widget.allow_mobile_webview?
origin = request.headers['Origin']
origin.blank? || origin == 'null' || origin&.start_with?('file://')
end
end
WidgetsController.prepend_mod_with('WidgetsController')
+5
View File
@@ -2,6 +2,7 @@
import ApiClient from './ApiClient';
const getTimeOffset = () => -new Date().getTimezoneOffset() / 60;
const getTimeZone = () => Intl.DateTimeFormat().resolvedOptions().timeZone;
class ReportsAPI extends ApiClient {
constructor() {
@@ -26,6 +27,7 @@ class ReportsAPI extends ApiClient {
id,
group_by: groupBy,
business_hours: businessHours,
timezone: getTimeZone(),
timezone_offset: getTimeOffset(),
},
});
@@ -41,6 +43,7 @@ class ReportsAPI extends ApiClient {
id,
group_by: groupBy,
business_hours: businessHours,
timezone: getTimeZone(),
timezone_offset: getTimeOffset(),
},
});
@@ -105,6 +108,8 @@ class ReportsAPI extends ApiClient {
type: 'account',
group_by: groupBy,
business_hours: businessHours,
timezone: getTimeZone(),
timezone_offset: getTimeOffset(),
},
});
}
@@ -2,6 +2,8 @@ import reportsAPI from '../reports';
import ApiClient from '../ApiClient';
describe('#Reports API', () => {
const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
it('creates correct instance', () => {
expect(reportsAPI).toBeInstanceOf(ApiClient);
expect(reportsAPI.apiVersion).toBe('/api/v2');
@@ -46,6 +48,7 @@ describe('#Reports API', () => {
since: 1621103400,
until: 1621621800,
type: 'account',
timezone,
timezone_offset: -0,
},
});
@@ -59,6 +62,7 @@ describe('#Reports API', () => {
group_by: undefined,
id: undefined,
since: 1621103400,
timezone,
timezone_offset: -0,
type: 'account',
until: 1621621800,
@@ -140,6 +144,8 @@ describe('#Reports API', () => {
type: 'account',
group_by: 'date',
business_hours: true,
timezone,
timezone_offset: -0,
},
}
);
@@ -0,0 +1,47 @@
import summaryReportsAPI from '../summaryReports';
import ApiClient from '../ApiClient';
describe('#Summary Reports API', () => {
const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
it('creates correct instance', () => {
expect(summaryReportsAPI).toBeInstanceOf(ApiClient);
expect(summaryReportsAPI.apiVersion).toBe('/api/v2');
});
describe('API calls', () => {
const originalAxios = window.axios;
const axiosMock = {
get: vi.fn(() => Promise.resolve()),
};
beforeEach(() => {
window.axios = axiosMock;
});
afterEach(() => {
window.axios = originalAxios;
});
it('includes timezone data in summary report requests', () => {
summaryReportsAPI.getAgentReports({
since: 1621103400,
until: 1621621800,
businessHours: true,
});
expect(axiosMock.get).toHaveBeenCalledWith(
'/api/v2/summary_reports/agent',
{
params: {
since: 1621103400,
until: 1621621800,
business_hours: true,
timezone,
timezone_offset: -0,
},
}
);
});
});
});
@@ -1,6 +1,9 @@
/* global axios */
import ApiClient from './ApiClient';
const getTimeOffset = () => -new Date().getTimezoneOffset() / 60;
const getTimeZone = () => Intl.DateTimeFormat().resolvedOptions().timeZone;
class SummaryReportsAPI extends ApiClient {
constructor() {
super('summary_reports', { accountScoped: true, apiVersion: 'v2' });
@@ -12,6 +15,8 @@ class SummaryReportsAPI extends ApiClient {
since,
until,
business_hours: businessHours,
timezone: getTimeZone(),
timezone_offset: getTimeOffset(),
},
});
}
@@ -22,6 +27,8 @@ class SummaryReportsAPI extends ApiClient {
since,
until,
business_hours: businessHours,
timezone: getTimeZone(),
timezone_offset: getTimeOffset(),
},
});
}
@@ -32,6 +39,8 @@ class SummaryReportsAPI extends ApiClient {
since,
until,
business_hours: businessHours,
timezone: getTimeZone(),
timezone_offset: getTimeOffset(),
},
});
}
@@ -42,6 +51,8 @@ class SummaryReportsAPI extends ApiClient {
since,
until,
business_hours: businessHours,
timezone: getTimeZone(),
timezone_offset: getTimeOffset(),
},
});
}
@@ -1,22 +1,8 @@
<script setup>
import LocaleCard from './LocaleCard.vue';
const locales = [
{
name: 'English',
code: 'en',
isDefault: true,
isDraft: false,
articleCount: 29,
categoryCount: 5,
},
{
name: 'Spanish',
code: 'es',
isDefault: false,
isDraft: true,
articleCount: 29,
categoryCount: 5,
},
{ name: 'English', isDefault: true, articleCount: 29, categoryCount: 5 },
{ name: 'Spanish', isDefault: false, articleCount: 29, categoryCount: 5 },
];
</script>
@@ -33,8 +19,6 @@ const locales = [
<LocaleCard
:locale="locale.name"
:is-default="locale.isDefault"
:is-draft="locale.isDraft"
:locale-code="locale.code"
:article-count="locale.articleCount"
:category-count="locale.categoryCount"
/>
@@ -2,7 +2,7 @@
import { computed } from 'vue';
import { useI18n } from 'vue-i18n';
import { useToggle } from '@vueuse/core';
import { buildLocaleMenuItems } from 'dashboard/helper/portalHelper';
import { LOCALE_MENU_ITEMS } from 'dashboard/helper/portalHelper';
import CardLayout from 'dashboard/components-next/CardLayout.vue';
import Button from 'dashboard/components-next/button/Button.vue';
@@ -17,10 +17,6 @@ const props = defineProps({
type: Boolean,
required: true,
},
isDraft: {
type: Boolean,
required: true,
},
localeCode: {
type: String,
required: true,
@@ -41,28 +37,11 @@ const { t } = useI18n();
const [showDropdownMenu, toggleDropdown] = useToggle();
const localeLabel = computed(() => `${props.locale} (${props.localeCode})`);
const localeMenuLabels = computed(() => ({
'change-default': t(
'HELP_CENTER.LOCALES_PAGE.LOCALE_CARD.DROPDOWN_MENU.MAKE_DEFAULT'
),
'move-to-draft': t(
'HELP_CENTER.LOCALES_PAGE.LOCALE_CARD.DROPDOWN_MENU.MOVE_TO_DRAFT'
),
'publish-locale': t(
'HELP_CENTER.LOCALES_PAGE.LOCALE_CARD.DROPDOWN_MENU.PUBLISH_LOCALE'
),
delete: t('HELP_CENTER.LOCALES_PAGE.LOCALE_CARD.DROPDOWN_MENU.DELETE'),
}));
const localeMenuItems = computed(() =>
buildLocaleMenuItems({
isDefault: props.isDefault,
isDraft: props.isDraft,
}).map(item => ({
LOCALE_MENU_ITEMS.map(item => ({
...item,
label: localeMenuLabels.value[item.action],
label: t(item.label),
disabled: props.isDefault,
}))
);
@@ -77,7 +56,7 @@ const handleAction = ({ action, value }) => {
<div class="flex justify-between gap-2">
<div class="flex items-center justify-start gap-2">
<span class="text-sm font-medium text-n-slate-12 line-clamp-1">
{{ localeLabel }}
{{ locale }} ({{ localeCode }})
</span>
<span
v-if="isDefault"
@@ -85,12 +64,6 @@ const handleAction = ({ action, value }) => {
>
{{ $t('HELP_CENTER.LOCALES_PAGE.LOCALE_CARD.DEFAULT') }}
</span>
<span
v-else-if="isDraft"
class="bg-n-alpha-2 h-6 inline-flex items-center justify-center rounded-md text-xs border-px border-transparent text-n-slate-11 px-2 py-0.5"
>
{{ $t('HELP_CENTER.LOCALES_PAGE.LOCALE_CARD.DRAFT') }}
</span>
</div>
<div class="flex items-center justify-end gap-4">
<div class="flex items-center gap-4">
@@ -113,7 +86,6 @@ const handleAction = ({ action, value }) => {
</span>
</div>
<div
v-if="localeMenuItems.length"
v-on-clickaway="() => toggleDropdown(false)"
class="relative group"
>
@@ -1,5 +1,5 @@
<script setup>
import { ref, computed, watch } from 'vue';
import { ref, computed } from 'vue';
import { useI18n } from 'vue-i18n';
import { useStore } from 'dashboard/composables/store';
import { useAlert, useTrack } from 'dashboard/composables';
@@ -24,20 +24,12 @@ const dialogRef = ref(null);
const isUpdating = ref(false);
const selectedLocale = ref('');
const localeStatus = ref('published');
const addedLocales = computed(() => {
const { allowed_locales: allowedLocales = [] } = props.portal?.config || {};
return allowedLocales.map(locale => locale.code);
});
const draftedLocales = computed(() => {
const { allowed_locales: allowedLocales = [] } = props.portal?.config || {};
return allowedLocales
.filter(locale => locale.draft)
.map(locale => locale.code);
});
const locales = computed(() => {
return Object.keys(allLocales)
.map(key => {
@@ -49,44 +41,17 @@ const locales = computed(() => {
.filter(locale => !addedLocales.value.includes(locale.value));
});
const statusOptions = computed(() => [
{
value: 'published',
label: t('HELP_CENTER.LOCALES_PAGE.ADD_LOCALE_DIALOG.STATUS.OPTIONS.LIVE'),
},
{
value: 'draft',
label: t('HELP_CENTER.LOCALES_PAGE.ADD_LOCALE_DIALOG.STATUS.OPTIONS.DRAFT'),
},
]);
const resetForm = () => {
selectedLocale.value = '';
localeStatus.value = 'published';
};
watch(localeStatus, value => {
if (!value) {
localeStatus.value = 'published';
}
});
const onCreate = async () => {
if (!selectedLocale.value) return;
isUpdating.value = true;
const updatedLocales = [...addedLocales.value, selectedLocale.value];
const updatedDraftLocales =
localeStatus.value === 'draft'
? [...new Set([...draftedLocales.value, selectedLocale.value])]
: draftedLocales.value;
try {
await store.dispatch('portals/update', {
portalSlug: props.portal?.slug,
config: {
allowed_locales: updatedLocales,
draft_locales: updatedDraftLocales,
default_locale: props.portal?.meta?.default_locale,
},
});
@@ -97,7 +62,7 @@ const onCreate = async () => {
from: route.name,
});
resetForm();
selectedLocale.value = '';
dialogRef.value?.close();
useAlert(
t('HELP_CENTER.LOCALES_PAGE.ADD_LOCALE_DIALOG.API.SUCCESS_MESSAGE')
@@ -122,7 +87,6 @@ defineExpose({ dialogRef });
type="edit"
:title="t('HELP_CENTER.LOCALES_PAGE.ADD_LOCALE_DIALOG.TITLE')"
:description="t('HELP_CENTER.LOCALES_PAGE.ADD_LOCALE_DIALOG.DESCRIPTION')"
@close="resetForm"
@confirm="onCreate"
>
<div class="flex flex-col gap-6">
@@ -134,16 +98,6 @@ defineExpose({ dialogRef });
"
class="[&>div>button:not(.focused)]:!outline-n-slate-5 [&>div>button:not(.focused)]:dark:!outline-n-slate-5"
/>
<div class="flex flex-col gap-2">
<span class="text-sm font-medium text-n-slate-12">
{{ t('HELP_CENTER.LOCALES_PAGE.ADD_LOCALE_DIALOG.STATUS.LABEL') }}
</span>
<ComboBox
v-model="localeStatus"
:options="statusOptions"
class="[&>div>button:not(.focused)]:!outline-n-slate-5 [&>div>button:not(.focused)]:dark:!outline-n-slate-5"
/>
</div>
</div>
</Dialog>
</template>
@@ -29,7 +29,6 @@ const isLocaleDefault = code => {
const updatePortalLocales = async ({
newAllowedLocales,
newDraftLocales,
defaultLocale,
messageKey,
}) => {
@@ -40,7 +39,6 @@ const updatePortalLocales = async ({
config: {
default_locale: defaultLocale,
allowed_locales: newAllowedLocales,
draft_locales: newDraftLocales,
},
});
@@ -55,12 +53,8 @@ const updatePortalLocales = async ({
const changeDefaultLocale = ({ localeCode }) => {
const newAllowedLocales = props.locales.map(locale => locale.code);
const newDraftLocales = props.locales
.filter(locale => locale.isDraft)
.map(locale => locale.code);
updatePortalLocales({
newAllowedLocales,
newDraftLocales,
defaultLocale: localeCode,
messageKey: 'CHANGE_DEFAULT_LOCALE',
});
@@ -87,15 +81,11 @@ const deletePortalLocale = async ({ localeCode }) => {
const updatedLocales = props.locales
.filter(locale => locale.code !== localeCode)
.map(locale => locale.code);
const updatedDraftLocales = props.locales
.filter(locale => locale.code !== localeCode && locale.isDraft)
.map(locale => locale.code);
const defaultLocale = props.portal.meta.default_locale;
await updatePortalLocales({
newAllowedLocales: updatedLocales,
newDraftLocales: updatedDraftLocales,
defaultLocale,
messageKey: 'DELETE_LOCALE',
});
@@ -108,46 +98,9 @@ const deletePortalLocale = async ({ localeCode }) => {
});
};
const updateDraftLocales = async ({ localeCode, shouldDraft, messageKey }) => {
const newAllowedLocales = props.locales.map(locale => locale.code);
const currentDraftLocales = props.locales
.filter(locale => locale.isDraft)
.map(locale => locale.code);
const newDraftLocales = shouldDraft
? [...new Set([...currentDraftLocales, localeCode])]
: currentDraftLocales.filter(locale => locale !== localeCode);
await updatePortalLocales({
newAllowedLocales,
newDraftLocales,
defaultLocale: props.portal.meta.default_locale,
messageKey,
});
};
const moveLocaleToDraft = async ({ localeCode }) => {
await updateDraftLocales({
localeCode,
shouldDraft: true,
messageKey: 'DRAFT_LOCALE',
});
};
const publishLocale = async ({ localeCode }) => {
await updateDraftLocales({
localeCode,
shouldDraft: false,
messageKey: 'PUBLISH_LOCALE',
});
};
const handleAction = ({ action }, localeCode) => {
if (action === 'change-default') {
changeDefaultLocale({ localeCode: localeCode });
} else if (action === 'move-to-draft') {
moveLocaleToDraft({ localeCode: localeCode });
} else if (action === 'publish-locale') {
publishLocale({ localeCode: localeCode });
} else if (action === 'delete') {
deletePortalLocale({ localeCode: localeCode });
}
@@ -161,7 +114,6 @@ const handleAction = ({ action }, localeCode) => {
:key="index"
:locale="locale.name"
:is-default="isLocaleDefault(locale.code)"
:is-draft="locale.isDraft"
:locale-code="locale.code"
:article-count="locale.articlesCount || 0"
:category-count="locale.categoriesCount || 0"
@@ -4,49 +4,37 @@ import LocalesPage from './LocalesPage.vue';
const locales = [
{
name: 'English (en-US)',
code: 'en',
isDefault: true,
isDraft: false,
articleCount: 5,
categoryCount: 5,
},
{
name: 'Spanish (es-ES)',
code: 'es',
isDefault: false,
isDraft: true,
articleCount: 20,
categoryCount: 10,
},
{
name: 'English (en-UK)',
code: 'en_GB',
isDefault: false,
isDraft: false,
articleCount: 15,
categoryCount: 7,
},
{
name: 'Malay (ms-MY)',
code: 'ms',
isDefault: false,
isDraft: false,
articleCount: 15,
categoryCount: 7,
},
{
name: 'Malayalam (ml-IN)',
code: 'ml',
isDefault: false,
isDraft: false,
articleCount: 10,
categoryCount: 5,
},
{
name: 'Hindi (hi-IN)',
code: 'hi',
isDefault: false,
isDraft: false,
articleCount: 15,
categoryCount: 7,
},
@@ -14,10 +14,6 @@ defineProps({
type: Boolean,
default: false,
},
hideToggle: {
type: Boolean,
default: false,
},
});
const modelValue = defineModel({ type: Boolean, default: false });
@@ -32,8 +28,7 @@ const modelValue = defineModel({ type: Boolean, default: false });
<span class="text-heading-3 text-n-slate-12">
{{ header }}
</span>
<div v-if="hideToggle" class="size-2" />
<ToggleSwitch v-else v-model="modelValue" />
<ToggleSwitch v-model="modelValue" />
</div>
<span v-if="description" class="text-body-main text-n-slate-11">
{{ description }}
@@ -27,7 +27,6 @@ const initialState = {
conversationFaqs: false,
memories: false,
citations: false,
contactAttributes: false,
},
};
@@ -60,7 +59,6 @@ const updateStateFromAssistant = assistant => {
conversationFaqs: config.feature_faq || false,
memories: config.feature_memory || false,
citations: config.feature_citation || false,
contactAttributes: config.feature_contact_attributes || false,
};
};
@@ -81,7 +79,6 @@ const handleBasicInfoUpdate = async () => {
feature_faq: state.features.conversationFaqs,
feature_memory: state.features.memories,
feature_citation: state.features.citations,
feature_contact_attributes: state.features.contactAttributes,
},
};
@@ -141,10 +138,6 @@ watch(
<input v-model="state.features.citations" type="checkbox" />
{{ t('CAPTAIN.ASSISTANTS.FORM.FEATURES.ALLOW_CITATIONS') }}
</label>
<label class="flex items-center gap-2">
<input v-model="state.features.contactAttributes" type="checkbox" />
{{ t('CAPTAIN.ASSISTANTS.FORM.FEATURES.ALLOW_CONTACT_ATTRIBUTES') }}
</label>
</div>
</div>
@@ -67,9 +67,6 @@ const isSent = computed(() => {
return sourceId.value && status.value === MESSAGE_STATUS.SENT;
}
// API inbox messages use real sent/delivered/read status values from the external system.
if (isAPIInbox.value) return status.value === MESSAGE_STATUS.SENT;
// All messages will be mark as sent for the Line channel, as there is no source ID.
if (isALineChannel.value) return true;
@@ -89,10 +86,8 @@ const isDelivered = computed(() => {
) {
return sourceId.value && status.value === MESSAGE_STATUS.DELIVERED;
}
// API inbox messages use real delivered status from the external system.
if (isAPIInbox.value) return status.value === MESSAGE_STATUS.DELIVERED;
// All messages marked as delivered for the web widget inbox once they are sent.
if (isAWebWidgetInbox.value) {
// All messages marked as delivered for the web widget inbox and API inbox once they are sent.
if (isAWebWidgetInbox.value || isAPIInbox.value) {
return status.value === MESSAGE_STATUS.SENT;
}
if (isALineChannel.value) {
@@ -828,8 +828,6 @@ onMounted(() => {
}
});
defineExpose({ focusEditorInputField });
// BUS Event to insert text or markdown into the editor at the
// current cursor position.
// Components using this
@@ -99,7 +99,6 @@ export default {
} = useUISettings();
const replyEditor = useTemplateRef('replyEditor');
const messageEditor = useTemplateRef('messageEditor');
const copilot = useCopilotReply();
const shortcutKey = useKbd(['$mod', '+', 'enter']);
@@ -110,7 +109,6 @@ export default {
setQuotedReplyFlagForInbox,
fetchQuotedReplyFlagFromUISettings,
replyEditor,
messageEditor,
copilot,
shortcutKey,
};
@@ -509,7 +507,7 @@ export default {
);
this.fetchAndSetReplyTo();
emitter.on(BUS_EVENTS.TOGGLE_REPLY_TO_MESSAGE, this.onReplyToMessage);
emitter.on(BUS_EVENTS.TOGGLE_REPLY_TO_MESSAGE, this.fetchAndSetReplyTo);
// A hacky fix to solve the drag and drop
// Is showing on top of new conversation modal drag and drop
@@ -524,7 +522,7 @@ export default {
unmounted() {
document.removeEventListener('paste', this.onPaste);
document.removeEventListener('keydown', this.handleKeyEvents);
emitter.off(BUS_EVENTS.TOGGLE_REPLY_TO_MESSAGE, this.onReplyToMessage);
emitter.off(BUS_EVENTS.TOGGLE_REPLY_TO_MESSAGE, this.fetchAndSetReplyTo);
emitter.off(BUS_EVENTS.INSERT_INTO_NORMAL_EDITOR, this.addIntoEditor);
emitter.off(
BUS_EVENTS.NEW_CONVERSATION_MODAL,
@@ -1193,15 +1191,6 @@ export default {
return false;
});
},
onReplyToMessage() {
this.fetchAndSetReplyTo();
if (this.inReplyTo) {
this.$nextTick(() => {
const pos = this.isSignatureEnabledForInbox ? 'start' : 'end';
this.messageEditor?.focusEditorInputField(pos);
});
}
},
resetReplyToMessage() {
const replyStorageKey = LOCAL_STORAGE_KEYS.MESSAGE_REPLY_TO;
LocalStorage.deleteFromJsonStore(replyStorageKey, this.conversationId);
@@ -1324,7 +1313,6 @@ export default {
/>
<WootMessageEditor
v-else-if="!showAudioRecorderEditor"
ref="messageEditor"
v-model="message"
:conversation-id="conversationId"
:editor-id="editorStateId"
@@ -32,7 +32,6 @@ const emit = defineEmits(['dismiss']);
xs
slate
icon="i-lucide-x"
class="flex-shrink-0"
@click.stop="emit('dismiss')"
/>
</div>
@@ -133,55 +133,20 @@ export const ARTICLE_TABS_OPTIONS = [
},
];
export const LOCALE_MENU_ITEMS = {
makeDefault: {
export const LOCALE_MENU_ITEMS = [
{
label: 'HELP_CENTER.LOCALES_PAGE.LOCALE_CARD.DROPDOWN_MENU.MAKE_DEFAULT',
action: 'change-default',
value: 'default',
icon: 'i-lucide-star',
},
moveToDraft: {
label: 'HELP_CENTER.LOCALES_PAGE.LOCALE_CARD.DROPDOWN_MENU.MOVE_TO_DRAFT',
action: 'move-to-draft',
value: 'draft',
icon: 'i-lucide-eye-off',
},
publishLocale: {
label: 'HELP_CENTER.LOCALES_PAGE.LOCALE_CARD.DROPDOWN_MENU.PUBLISH_LOCALE',
action: 'publish-locale',
value: 'publish',
icon: 'i-lucide-eye',
},
delete: {
{
label: 'HELP_CENTER.LOCALES_PAGE.LOCALE_CARD.DROPDOWN_MENU.DELETE',
action: 'delete',
value: 'delete',
icon: 'i-lucide-trash',
},
};
const disableLocaleMenuItems = menuItems =>
menuItems.map(item => ({ ...item, disabled: true }));
export const buildLocaleMenuItems = ({ isDefault, isDraft }) => {
if (isDefault) {
return disableLocaleMenuItems([
LOCALE_MENU_ITEMS.makeDefault,
LOCALE_MENU_ITEMS.moveToDraft,
LOCALE_MENU_ITEMS.delete,
]);
}
if (isDraft) {
return [LOCALE_MENU_ITEMS.publishLocale, LOCALE_MENU_ITEMS.delete];
}
return [
LOCALE_MENU_ITEMS.makeDefault,
LOCALE_MENU_ITEMS.moveToDraft,
LOCALE_MENU_ITEMS.delete,
];
};
];
export const ARTICLE_EDITOR_STATUS_OPTIONS = {
published: ['archive', 'draft'],
@@ -166,8 +166,6 @@ const TOD_TO_MERIDIEM = {
evening: 'pm',
night: 'pm',
};
const CJK_CHAR_RE =
/[\p{Script=Han}\p{Script=Hiragana}\p{Script=Katakana}\p{Script=Hangul}]/u;
// ─── Translation Cache ──────────────────────────────────────────────────────
@@ -280,13 +278,8 @@ const escapeRegex = s => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const substituteLocalTokens = (text, pairs) => {
let r = text;
pairs.forEach(([local, en]) => {
if (CJK_CHAR_RE.test(local)) {
const re = new RegExp(escapeRegex(local), 'g');
r = r.replace(re, ` ${en} `);
} else {
const re = new RegExp(`(?<=^|\\s)${escapeRegex(local)}(?=\\s|$)`, 'g');
r = r.replace(re, en);
}
const re = new RegExp(`(?<=^|\\s)${escapeRegex(local)}(?=\\s|$)`, 'g');
r = r.replace(re, en);
});
return r;
};
@@ -82,9 +82,6 @@ const ORDINAL_RE = `(\\d{1,2}(?:st|nd|rd|th)?|${ORDINAL_WORDS})`;
const HALF_UNIT_RE = /^(?:in\s+)?half\s+(?:an?\s+)?(hour|day|week|month|year)$/;
const RELATIVE_DURATION_RE = new RegExp(`^(?:in\\s+)?${NUM_RE}\\s+${UNIT_RE}$`);
const RELATIVE_DURATION_AFTER_RE = new RegExp(
`^(?:in\\s+)?${NUM_RE}\\s+${UNIT_RE}\\s+after$`
);
const DURATION_FROM_NOW_RE = new RegExp(
`^${NUM_RE}\\s+${UNIT_RE}\\s+from\\s+now$`
);
@@ -92,9 +89,6 @@ const RELATIVE_DAY_ONLY_RE = new RegExp(`^(${RELATIVE_DAYS})$`);
const RELATIVE_DAY_TOD_RE = new RegExp(
`^(${RELATIVE_DAYS})\\s+(?:at\\s+)?(${TIME_OF_DAY_NAMES})$`
);
const RELATIVE_DAY_MERIDIEM_RE = new RegExp(
`^(${RELATIVE_DAYS})\\s+(?:at\\s+)?(am|pm)$`
);
const RELATIVE_DAY_TOD_TIME_RE = new RegExp(
`^(${RELATIVE_DAYS})\\s+(?:at\\s+)?(${TIME_OF_DAY_NAMES})\\s+(\\d{1,2}(?::\\d{2})?)$`
);
@@ -251,7 +245,6 @@ const matchDuration = (text, now) => {
return (
parseDuration(text.match(DURATION_FROM_NOW_RE), now) ||
parseDuration(text.match(RELATIVE_DURATION_AFTER_RE), now) ||
parseDuration(text.match(RELATIVE_DURATION_RE), now)
);
};
@@ -310,13 +303,6 @@ const matchRelativeDay = (text, now) => {
);
}
const dayMeridiemMatch = text.match(RELATIVE_DAY_MERIDIEM_RE);
if (dayMeridiemMatch) {
const [, dayKey, meridiem] = dayMeridiemMatch;
const hours = meridiem === 'am' ? 9 : 14;
return applyTimeWithRollover(RELATIVE_DAY_MAP[dayKey], hours, 0, now);
}
const dayAtTimeMatch = text.match(RELATIVE_DAY_AT_TIME_RE);
if (dayAtTimeMatch) {
const [, dayKey, timeRaw] = dayAtTimeMatch;
@@ -1,8 +1,4 @@
import {
buildLocaleMenuItems,
buildPortalArticleURL,
buildPortalURL,
} from '../portalHelper';
import { buildPortalArticleURL, buildPortalURL } from '../portalHelper';
describe('PortalHelper', () => {
describe('buildPortalURL', () => {
@@ -72,39 +68,4 @@ describe('PortalHelper', () => {
).toEqual('https://app.chatwoot.com/hc/handbook/articles/article-slug');
});
});
describe('buildLocaleMenuItems', () => {
it('returns disabled actions for the default locale', () => {
expect(
buildLocaleMenuItems({
isDefault: true,
isDraft: false,
})
).toEqual(
expect.arrayContaining([
expect.objectContaining({ action: 'change-default', disabled: true }),
expect.objectContaining({ action: 'move-to-draft', disabled: true }),
expect.objectContaining({ action: 'delete', disabled: true }),
])
);
});
it('returns publish and delete actions for draft locales', () => {
expect(
buildLocaleMenuItems({
isDefault: false,
isDraft: true,
}).map(({ action }) => action)
).toEqual(['publish-locale', 'delete']);
});
it('returns default, draft, and delete actions for live locales', () => {
expect(
buildLocaleMenuItems({
isDefault: false,
isDraft: false,
}).map(({ action }) => action)
).toEqual(['change-default', 'move-to-draft', 'delete']);
});
});
});
@@ -1626,24 +1626,6 @@ describe('generateDateSuggestions — localized input regressions', () => {
},
};
const zhTWSnoozeTranslations = {
UNITS: {
HOUR: '小時',
HOURS: '小時',
DAY: '天',
DAYS: '天',
},
HALF: '半',
RELATIVE: {
TOMORROW: '明天',
},
MERIDIEM: {
AM: '上午',
PM: '下午',
},
AFTER: '後',
};
describe('P1: short non-English tokens must NOT produce spurious half-duration suggestions', () => {
it('Arabic "غد" does not produce half-duration suggestions', () => {
const results = generateDateSuggestions('غد', now, {
@@ -1739,37 +1721,6 @@ describe('generateDateSuggestions — localized input regressions', () => {
expect(results[0].date.getHours()).toBe(6);
});
});
describe('zh_TW compact CJK inputs', () => {
const options = {
translations: zhTWSnoozeTranslations,
locale: 'zh-TW',
};
it('parses "2小時後" (2 hours from now) without spaces', () => {
const results = generateDateSuggestions('2小時後', now, options);
expect(results.length).toBeGreaterThan(0);
expect(results[0].date.getDate()).toBe(16);
expect(results[0].date.getHours()).toBe(12);
expect(results[0].date.getMinutes()).toBe(0);
});
it('parses "半天" (half day) without spaces', () => {
const results = generateDateSuggestions('半天', now, options);
expect(results.length).toBeGreaterThan(0);
expect(results[0].date.getDate()).toBe(16);
expect(results[0].date.getHours()).toBe(22);
expect(results[0].date.getMinutes()).toBe(0);
});
it('parses "明天 上午" (tomorrow AM) into tomorrow 9am', () => {
const results = generateDateSuggestions('明天 上午', now, options);
expect(results.length).toBeGreaterThan(0);
expect(results[0].date.getDate()).toBe(17);
expect(results[0].date.getHours()).toBe(9);
expect(results[0].date.getMinutes()).toBe(0);
});
});
});
describe('no-space duration suggestions', () => {
@@ -4,9 +4,6 @@
"LOADING_EDITOR": "Loading editor...",
"DESCRIPTION": "Agent Bots are like the most fabulous members of your team. They can handle the small stuff, so you can focus on the stuff that matters. Give them a try. You can manage your bots from this page or create new ones using the 'Add Bot' button.",
"LEARN_MORE": "Learn about agent bots",
"COUNT": "{n} bot | {n} bots",
"SEARCH_PLACEHOLDER": "Search bots...",
"NO_RESULTS": "No bots found matching your search",
"GLOBAL_BOT": "System bot",
"GLOBAL_BOT_BADGE": "System",
"AVATAR": {
@@ -37,8 +34,7 @@
"LOADING": "Fetching bots...",
"TABLE_HEADER": {
"DETAILS": "Bot Details",
"URL": "Webhook URL",
"ACTIONS": "Actions"
"URL": "Webhook URL"
}
},
"DELETE": {
@@ -9,7 +9,6 @@
"ADMINISTRATOR": "Administrator",
"AGENT": "Agent"
},
"COUNT": "{n} agent | {n} agents",
"LIST": {
"404": "There are no agents associated to this account",
"TITLE": "Manage agents in your team",
@@ -97,8 +96,6 @@
"ERROR_MESSAGE": "Could not connect to Woot Server, Please try again later"
}
},
"SEARCH_PLACEHOLDER": "Search agents...",
"NO_RESULTS": "No agents found matching your search",
"SEARCH": {
"NO_RESULTS": "No results found."
},
@@ -5,9 +5,6 @@
"LOADING": "Fetching custom attributes",
"DESCRIPTION": "A custom attribute tracks additional details about your contacts or conversations—such as the subscription plan or the date of their first purchase. You can add different types of custom attributes, such as text, lists, or numbers, to capture the specific information you need.",
"LEARN_MORE": "Learn more about custom attributes",
"COUNT": "{n} attribute | {n} attributes",
"SEARCH_PLACEHOLDER": "Search attributes...",
"NO_RESULTS": "No attributes found matching your search",
"ATTRIBUTE_MODELS": {
"CONVERSATION": "Conversation",
"CONTACT": "Contact"
@@ -66,10 +63,6 @@
},
"ENABLE_REGEX": {
"LABEL": "Enable regex validation"
},
"BADGES": {
"PRE_CHAT": "Pre-chat",
"RESOLUTION": "Resolution"
}
},
"API": {
@@ -3,11 +3,8 @@
"HEADER": "Automation",
"DESCRIPTION": "Automation can replace and streamline existing processes that require manual effort, such as adding labels and assigning conversations to the most suitable agent. This allows the team to focus on their strengths while reducing time spent on routine tasks.",
"LEARN_MORE": "Learn more about automation",
"COUNT": "{n} automation | {n} automations",
"HEADER_BTN_TXT": "Create Automation",
"HEADER_BTN_TXT": "Add Automation Rule",
"LOADING": "Fetching automation rules",
"SEARCH_PLACEHOLDER": "Search automation rules...",
"NO_RESULTS": "No automation rules found matching your search",
"ADD": {
"TITLE": "Add Automation Rule",
"SUBMIT": "Create",
@@ -45,9 +42,9 @@
"LIST": {
"TABLE_HEADER": {
"NAME": "Name",
"DESCRIPTION": "Description",
"ACTIVE": "Active",
"CREATED_ON": "Created on",
"ACTIONS": "Actions"
"CREATED_ON": "Created on"
},
"404": "No automation rules found"
},
@@ -153,8 +150,7 @@
"ADD_PRIVATE_NOTE": "Add a Private Note",
"CHANGE_PRIORITY": "Change Priority",
"ADD_SLA": "Add SLA",
"OPEN_CONVERSATION": "Open conversation",
"PENDING_CONVERSATION": "Mark conversation as pending"
"OPEN_CONVERSATION": "Open conversation"
},
"MESSAGE_TYPES": {
"INCOMING": "Incoming Message",
@@ -22,10 +22,6 @@
"UPDATE_SUCCESFUL": "Conversation status updated successfully.",
"UPDATE_FAILED": "Failed to update conversations. Please try again."
},
"RESOLVE": {
"ALL_MISSING_ATTRIBUTES": "Cannot resolve conversations due to missing required attributes",
"PARTIAL_SUCCESS": "Some conversations need required attributes before resolving and were skipped"
},
"LABELS": {
"ASSIGN_LABELS": "Assign labels",
"NO_LABELS_FOUND": "No labels found",
@@ -3,11 +3,8 @@
"HEADER": "Canned Responses",
"LEARN_MORE": "Learn more about canned responses",
"DESCRIPTION": "Canned Responses are pre-written reply templates that help you quickly respond to a conversation. Agents can type the '/' character followed by the shortcode to insert a canned response during a conversation. ",
"COUNT": "{n} canned response | {n} canned responses",
"HEADER_BTN_TXT": "Add canned response",
"LOADING": "Fetching canned responses...",
"SEARCH_PLACEHOLDER": "Search canned responses...",
"NO_RESULTS": "No canned responses found matching your search",
"SEARCH_404": "There are no items matching this query.",
"LIST": {
"404": "There are no canned responses available in this account.",
@@ -76,9 +76,6 @@
},
"waiting_since_desc": {
"TEXT": "Pending Response: Shortest first"
},
"priority_desc_created_at_asc": {
"TEXT": "Priority: Highest first, Created: Oldest first"
}
},
"ATTACHMENTS": {
File diff suppressed because it is too large Load Diff
@@ -61,7 +61,6 @@
"UNSUPPORTED_MESSAGE": "This message is unsupported. You can view this message on the Facebook / Instagram app.",
"UNSUPPORTED_MESSAGE_FACEBOOK": "This message is unsupported. You can view this message on the Facebook Messenger app.",
"UNSUPPORTED_MESSAGE_INSTAGRAM": "This message is unsupported. You can view this message on the Instagram app.",
"UNSUPPORTED_MESSAGE_TIKTOK": "This message is unsupported. You can view this message on the TikTok app.",
"SUCCESS_DELETE_MESSAGE": "Message deleted successfully",
"FAIL_DELETE_MESSSAGE": "Couldn't delete message! Try again",
"NO_RESPONSE": "No response",
@@ -174,10 +173,6 @@
"SUCCESFUL": "Assigned label #{labelName} to conversation id {conversationId}",
"FAILED": "Couldn't assign label. Please try again."
},
"LABEL_REMOVAL": {
"SUCCESFUL": "Removed label #{labelName} from conversation id {conversationId}",
"FAILED": "Couldn't remove label. Please try again."
},
"TEAM_ASSIGNMENT": {
"SUCCESFUL": "Assigned team \"{team}\" to conversation id {conversationId}",
"FAILED": "Couldn't assign team. Please try again."
@@ -190,11 +185,7 @@
"DISABLE_SIGN_TOOLTIP": "Disable signature",
"MSG_INPUT": "Shift + enter for new line. Start with '/' to select a Canned Response.",
"PRIVATE_MSG_INPUT": "Shift + enter for new line. This will be visible only to Agents",
"MESSAGING_RESTRICTED": "You cannot reply to this conversation",
"MESSAGING_RESTRICTED_WHATSAPP": "You can only reply using a template message due to 24-hour message window restriction",
"MESSAGING_RESTRICTED_API": "You can only reply using a template message due to message window restriction",
"MESSAGE_SIGNATURE_NOT_CONFIGURED": "Message signature is not configured, please configure it in profile settings.",
"COPILOT_MSG_INPUT": "Give copilot additional prompts, or ask anything else... Press enter to send follow-up",
"CLICK_HERE": "Click here to update",
"WHATSAPP_TEMPLATES": "Whatsapp Templates"
},
@@ -214,7 +205,7 @@
"DRAG_DROP": "Drag and drop here to attach",
"START_AUDIO_RECORDING": "Start audio recording",
"STOP_AUDIO_RECORDING": "Stop audio recording",
"COPILOT_THINKING": "Copilot is thinking",
"": "",
"EMAIL_HEAD": {
"TO": "TO",
"ADD_BCC": "Add bcc",
@@ -256,12 +247,9 @@
"SUCCESS_DELETE_CONVERSATION": "ውይይት በተሳካ ሁኔታ ተሰርዟል",
"FAIL_DELETE_CONVERSATION": "ውይይትን መሰረዝ አልተቻለም! እንደገና ይሞክሩ",
"FILE_SIZE_LIMIT": "File exceeds the {MAXIMUM_SUPPORTED_FILE_UPLOAD_SIZE} MB attachment limit",
"FILE_TYPE_NOT_SUPPORTED": "This {fileName} file type is not supported in this conversation",
"MESSAGE_ERROR": "Unable to send this message, please try again later",
"SENT_BY": "Sent by:",
"BOT": "Bot",
"NATIVE_APP": "Native app",
"NATIVE_APP_ADVISORY": "This message was sent from the native app. Reply from Chatwoot to maintain the message window.",
"SEND_FAILED": "Couldn't send message! Try again",
"TRY_AGAIN": "retry",
"ASSIGNMENT": {
@@ -306,7 +294,6 @@
"CANCEL": "Cancel",
"SEND_EMAIL_SUCCESS": "The chat transcript was sent successfully",
"SEND_EMAIL_ERROR": "There was an error, please try again",
"SEND_EMAIL_PAYMENT_REQUIRED": "Email transcript is not available on your current plan. Please upgrade to use this feature.",
"FORM": {
"SEND_TO_CONTACT": "Send the transcript to the customer",
"SEND_TO_AGENT": "Send the transcript to the assigned agent",
@@ -3,11 +3,8 @@
"HEADER": "Custom Roles",
"LEARN_MORE": "Learn more about custom roles",
"DESCRIPTION": "Custom roles are roles that are created by the account owner or admin. These roles can be assigned to agents to define their access and permissions within the account. Custom roles can be created with specific permissions and access levels to suit the requirements of the organization.",
"COUNT": "{n} custom role | {n} custom roles",
"HEADER_BTN_TXT": "Add custom role",
"LOADING": "Fetching custom roles...",
"SEARCH_PLACEHOLDER": "Search custom roles...",
"NO_RESULTS": "No custom roles found matching your search",
"SEARCH_404": "There are no items matching this query.",
"PAYWALL": {
"TITLE": "Upgrade to create custom roles",
@@ -1,8 +1,5 @@
{
"DATE_PICKER": {
"PREVIOUS_PERIOD": "Previous period",
"NEXT_PERIOD": "Next period",
"WEEK_NUMBER": "Week #{weekNumber}",
"APPLY_BUTTON": "Apply",
"CLEAR_BUTTON": "Clear",
"DATE_RANGE_INPUT": {
@@ -16,8 +13,6 @@
"LAST_3_MONTHS": "Last 3 months",
"LAST_6_MONTHS": "Last 6 months",
"LAST_YEAR": "Last year",
"THIS_WEEK": "This week",
"MONTH_TO_DATE": "This month",
"CUSTOM_RANGE": "Custom date range"
}
}
@@ -7,13 +7,6 @@
},
"CLOSE": "Close",
"BETA": "Beta",
"BETA_DESCRIPTION": "This feature is in beta and may change as we improve it.",
"ACCEPT": "Accept",
"DISCARD": "Discard",
"PREFERRED": "Preferred"
},
"CHOICE_TOGGLE": {
"YES": "Yes",
"NO": "No"
"BETA_DESCRIPTION": "This feature is in beta and may change as we improve it."
}
}
@@ -182,7 +182,6 @@
},
"COMMAND_BAR": {
"SEARCH_PLACEHOLDER": "Search or jump to",
"SNOOZE_PLACEHOLDER": "Type a time e.g. tomorrow, 2 hours, next friday, jan 15...",
"SECTIONS": {
"GENERAL": "General",
"REPORTS": "Reports",
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -3,9 +3,6 @@
"FETCHING": "Fetching Integrations",
"NO_HOOK_CONFIGURED": "There are no {integrationId} integrations configured in this account.",
"HEADER": "Applications",
"COUNT": "{n} integration | {n} integrations",
"SEARCH_PLACEHOLDER": "Search...",
"NO_RESULTS": "No results found matching your search",
"STATUS": {
"ENABLED": "Enabled",
"DISABLED": "Disabled"
@@ -34,7 +31,6 @@
"LIST": {
"FETCHING": "Fetching integration hooks",
"INBOX": "Inbox",
"ACTIONS": "Actions",
"DELETE": {
"BUTTON_TEXT": "Delete"
}
File diff suppressed because it is too large Load Diff
@@ -5,9 +5,6 @@
"LOADING": "Fetching labels",
"DESCRIPTION": "Labels help you categorize and prioritize conversations and leads. You can assign a label to a conversation or contact using the side panel.",
"LEARN_MORE": "Learn more about labels",
"COUNT": "{n} label | {n} labels",
"SEARCH_PLACEHOLDER": "Search labels...",
"NO_RESULTS": "No labels found matching your search",
"SEARCH_404": "There are no items matching this query",
"LIST": {
"404": "There are no labels available in this account.",
@@ -16,8 +13,7 @@
"TABLE_HEADER": {
"NAME": "Name",
"DESCRIPTION": "Description",
"COLOR": "Color",
"ACTION": "Actions"
"COLOR": "Color"
}
},
"FORM": {
@@ -3,12 +3,9 @@
"HEADER": "Macros",
"DESCRIPTION": "A macro is a set of saved actions that help customer service agents easily complete tasks. The agents can define a set of actions like tagging a conversation with a label, sending an email transcript, updating a custom attribute, etc., and they can run these actions in a single click.",
"LEARN_MORE": "Learn more about macros",
"COUNT": "{n} macro | {n} macros",
"HEADER_BTN_TXT": "Add a new macro",
"HEADER_BTN_TXT_SAVE": "Save macro",
"LOADING": "Fetching macros",
"SEARCH_PLACEHOLDER": "Search macros...",
"NO_RESULTS": "No macros found matching your search",
"ERROR": "Something went wrong. Please try again",
"ORDER_INFO": "Macros will run in the order you add your actions. You can rearrange them by dragging them by the handle beside each node.",
"ADD": {
@@ -32,8 +29,7 @@
"NAME": "Name",
"CREATED BY": "Created by",
"LAST_UPDATED_BY": "Last updated by",
"VISIBILITY": "Visibility",
"ACTIONS": "Actions"
"VISIBILITY": "Visibility"
},
"404": "No macros found"
},
@@ -1,7 +1,7 @@
{
"MFA_SETTINGS": {
"TITLE": "Two-Factor Authentication",
"SUBTITLE": "Protect your account from unauthorized access with TOTP-based authentication. This adds an extra layer of security to your account.",
"SUBTITLE": "Secure your account with TOTP-based authentication",
"DESCRIPTION": "Add an extra layer of security to your account using a time-based one-time password (TOTP)",
"STATUS_TITLE": "Authentication Status",
"STATUS_DESCRIPTION": "Manage your two-factor authentication settings and backup recovery codes",
@@ -1,39 +1,39 @@
{
"REPORT": {
"HEADER": "Conversations",
"LOADING_CHART": "ገበታ ውሂብ በማስጫን ላይ...",
"NO_ENOUGH_DATA": "ሪፖርት ለማመንጨት በቂ ውሂብ ነጥቦች አልደረሰንም፣ እባክዎ በኋላ ደግመው ይሞክሩ።",
"DOWNLOAD_CONVERSATION_REPORTS": "Download conversation reports",
"LOADING_CHART": "Loading chart data...",
"NO_ENOUGH_DATA": "We've not received enough data points to generate report, Please try again later.",
"DOWNLOAD_AGENT_REPORTS": "Download agent reports",
"DATA_FETCHING_FAILED": "Failed to fetch data, please try again later.",
"SUMMARY_FETCHING_FAILED": "Failed to fetch summary, please try again later.",
"METRICS": {
"CONVERSATIONS": {
"NAME": "ውይይቶች",
"DESC": "( ጠቅላላ )"
"NAME": "Conversations",
"DESC": "( Total )"
},
"INCOMING_MESSAGES": {
"NAME": "Messages received",
"DESC": "( ጠቅላላ )"
"DESC": "( Total )"
},
"OUTGOING_MESSAGES": {
"NAME": "Messages sent",
"DESC": "( ጠቅላላ )"
"DESC": "( Total )"
},
"FIRST_RESPONSE_TIME": {
"NAME": "First Response Time",
"DESC": "( አማካይ )",
"DESC": "( Avg )",
"INFO_TEXT": "Total number of conversations used for computation:",
"TOOLTIP_TEXT": "First Response Time is {metricValue} (based on {conversationCount} conversations)"
},
"RESOLUTION_TIME": {
"NAME": "የመፍትሄ ጊዜ",
"DESC": "( አማካይ )",
"NAME": "Resolution Time",
"DESC": "( Avg )",
"INFO_TEXT": "Total number of conversations used for computation:",
"TOOLTIP_TEXT": "Resolution Time is {metricValue} (based on {conversationCount} conversations)"
},
"RESOLUTION_COUNT": {
"NAME": "የመፍትሄ ብዛት",
"DESC": "( ጠቅላላ )"
"NAME": "Resolution Count",
"DESC": "( Total )"
},
"BOT_RESOLUTION_COUNT": {
"NAME": "Resolution Count",
@@ -61,8 +61,8 @@
"CUSTOM_DATE_RANGE": "Custom date range"
},
"CUSTOM_DATE_RANGE": {
"CONFIRM": "አተግባር ላይ አውርድ",
"PLACEHOLDER": "የቀን ክልል ይምረጡ"
"CONFIRM": "Apply",
"PLACEHOLDER": "Select date range"
},
"GROUP_BY_FILTER_DROPDOWN_LABEL": "Group By",
"DURATION_FILTER_LABEL": "Duration",
@@ -127,33 +127,28 @@
}
},
"AGENT_REPORTS": {
"HEADER": "የወኪሎች እይታ",
"DESCRIPTION": "Easily track agent performance with key metrics such as conversations, response times, resolution times, and resolved cases. Click an agent's name to learn more.",
"LOADING_CHART": "የቅርጸ ቁምፊ ውሂብ በመጫን ላይ...",
"NO_ENOUGH_DATA": "ሪፖርት ለማቅረብ በቂ ውሂብ አልደረሰንም፣ እባክዎ በኋላ ደግመው ይሞክሩ።.",
"DOWNLOAD_AGENT_REPORTS": "የAgent ሪፖርቶችን አውርድ",
"FILTER_DROPDOWN_LABEL": "Agent ይምረጡ",
"FILTERS": {
"INPUT_PLACEHOLDER": {
"AGENTS": "Search agents"
}
},
"HEADER": "Agents Overview",
"DESCRIPTION": "Easily track agent performance with key metrics such as conversations, response times, resolution times, and resolved cases. Click an agents name to learn more.",
"LOADING_CHART": "Loading chart data...",
"NO_ENOUGH_DATA": "We've not received enough data points to generate report, Please try again later.",
"DOWNLOAD_AGENT_REPORTS": "Download agent reports",
"FILTER_DROPDOWN_LABEL": "Select Agent",
"METRICS": {
"CONVERSATIONS": {
"NAME": "ውይይቶች",
"DESC": "( ጠቅላላ )"
"NAME": "Conversations",
"DESC": "( Total )"
},
"INCOMING_MESSAGES": {
"NAME": "የመጣ መልእክቶች",
"DESC": "( ጠቅላላ )"
"NAME": "Incoming Messages",
"DESC": "( Total )"
},
"OUTGOING_MESSAGES": {
"NAME": "የሚያልኩ መልእክቶች",
"DESC": "( ጠቅላላ )"
"NAME": "Outgoing Messages",
"DESC": "( Total )"
},
"FIRST_RESPONSE_TIME": {
"NAME": "First Response Time",
"DESC": "( አማካይ )",
"DESC": "( Avg )",
"INFO_TEXT": "Total number of conversations used for computation:",
"TOOLTIP_TEXT": "First Response Time is {metricValue} (based on {conversationCount} conversations)"
},
@@ -206,11 +201,6 @@
"NO_ENOUGH_DATA": "We've not received enough data points to generate report, Please try again later.",
"DOWNLOAD_LABEL_REPORTS": "Download label reports",
"FILTER_DROPDOWN_LABEL": "Select Label",
"FILTERS": {
"INPUT_PLACEHOLDER": {
"LABELS": "Search labels"
}
},
"METRICS": {
"CONVERSATIONS": {
"NAME": "Conversations",
@@ -281,11 +271,6 @@
"FILTER_DROPDOWN_LABEL": "Select Inbox",
"ALL_INBOXES": "All Inboxes",
"SEARCH_INBOX": "Search Inbox",
"FILTERS": {
"INPUT_PLACEHOLDER": {
"INBOXES": "Search inboxes"
}
},
"METRICS": {
"CONVERSATIONS": {
"NAME": "Conversations",
@@ -349,19 +334,11 @@
},
"TEAM_REPORTS": {
"HEADER": "Team Overview",
"DESCRIPTION": "Get a snapshot of your team's performance with essential metrics, including conversations, response times, resolution times, and resolved cases. Click a team name for more details.",
"DESCRIPTION": "Get a snapshot of your teams performance with essential metrics, including conversations, response times, resolution times, and resolved cases. Click a team name for more details.",
"LOADING_CHART": "Loading chart data...",
"NO_ENOUGH_DATA": "We've not received enough data points to generate report, Please try again later.",
"DOWNLOAD_TEAM_REPORTS": "Download team reports",
"FILTER_DROPDOWN_LABEL": "Select Team",
"FILTERS": {
"ADD_FILTER": "Add filter",
"CLEAR_ALL": "Clear all",
"NO_FILTER": "No filters available",
"INPUT_PLACEHOLDER": {
"TEAMS": "Search teams"
}
},
"METRICS": {
"CONVERSATIONS": {
"NAME": "Conversations",
@@ -424,80 +401,35 @@
}
},
"CSAT_REPORTS": {
"HEADER": "CSAT ሪፖርቶች",
"NO_RECORDS": "No responses yet",
"NO_RECORDS_DESCRIPTION": "CSAT survey responses will appear here once customers start providing feedback.",
"HEADER": "CSAT Reports",
"NO_RECORDS": "There are no CSAT survey responses available.",
"DOWNLOAD": "Download CSAT Reports",
"DOWNLOAD_FAILED": "Failed to download CSAT Reports",
"FILTERS": {
"ADD_FILTER": "Add filter",
"CLEAR_ALL": "Clear all",
"NO_FILTER": "No filters available",
"INPUT_PLACEHOLDER": {
"AGENTS": "Search agents",
"INBOXES": "Search inboxes",
"TEAMS": "Search teams",
"RATINGS": "Search ratings"
},
"AGENTS": {
"LABEL": "Agent"
},
"INBOXES": {
"LABEL": "Inbox"
},
"TEAMS": {
"LABEL": "Team"
},
"RATINGS": {
"LABEL": "Rating"
"PLACEHOLDER": "Choose Agents"
}
},
"TABLE": {
"HEADER": {
"CONTACT_NAME": "እውቂያ",
"AGENT_NAME": "Agent",
"RATING": "እምነት ደረጃ",
"FEEDBACK_TEXT": "አስተያየት አስተያየት",
"CONVERSATION": "Conversation",
"CUSTOMER": "Customer",
"RESPONSE": "Response",
"HANDLED_BY": "Handled by"
},
"UNKNOWN_CUSTOMER": "Unknown customer"
"CONTACT_NAME": "Contact",
"AGENT_NAME": "Assigned agent",
"RATING": "Rating",
"FEEDBACK_TEXT": "Feedback comment"
}
},
"NO_AGENT": "No assigned agent",
"NO_FEEDBACK": "No feedback provided",
"METRIC": {
"TOTAL_RESPONSES": {
"LABEL": "ጠቅላላ ምላሾች",
"TOOLTIP": "የተሰበሰበው ምላሾች ጠቅላላ ብዛት"
"LABEL": "Total responses",
"TOOLTIP": "Total number of responses collected"
},
"SATISFACTION_SCORE": {
"LABEL": "የደስታ ነጥብ",
"TOOLTIP": "አጠቃላይ የአዎንታዊ ምላሾች ብዛት / አጠቃላይ የምላሾች ብዛት * 100"
"LABEL": "Satisfaction score",
"TOOLTIP": "Total number of positive responses / Total number of responses * 100"
},
"RESPONSE_RATE": {
"LABEL": "የምላሽ ተመን",
"TOOLTIP": "አጠቃላይ የምላሾች ብዛት / አጠቃላይ የተላኩ የCSAT እቅድ መልእክቶች ብዛት * 100"
},
"RATING_DISTRIBUTION": "Rating distribution"
},
"REVIEW_NOTES": {
"TITLE": "Review notes",
"PLACEHOLDER": "Add review notes about this rating...",
"SAVE": "Save",
"CANCEL": "Cancel",
"SAVING": "Saving...",
"SAVED": "Notes saved successfully",
"SAVE_ERROR": "Failed to save notes",
"UPDATED_BY": "Updated by {name} {time}",
"UPDATED_BY_LABEL": "Updated by",
"PAYWALL": {
"TITLE": "Upgrade to add review notes",
"AVAILABLE_ON": "The review notes feature is only available in the Business and Enterprise plans.",
"UPGRADE_PROMPT": "Add internal context to every CSAT response with review notes. Capture what really happened, spot patterns faster, and make better decisions from your feedback.",
"UPGRADE_NOW": "Upgrade now",
"CANCEL_ANYTIME": "You can change or cancel your plan anytime"
"LABEL": "Response rate",
"TOOLTIP": "Total number of responses / Total number of CSAT survey messages sent * 100"
}
}
},
File diff suppressed because it is too large Load Diff
@@ -1,7 +1,6 @@
{
"REGISTER": {
"TRY_WOOT": "Create an account",
"GET_STARTED": "Get started with Chatwoot",
"TITLE": "Register",
"TESTIMONIAL_HEADER": "All it takes is one step to move forward",
"TESTIMONIAL_CONTENT": "You're one step away from engaging your customers, retaining them and finding new ones.",
@@ -5,12 +5,7 @@
"ADD_ACTION_LONG": "Create a new SLA Policy",
"DESCRIPTION": "Service Level Agreements (SLAs) are contracts that define clear expectations between your team and customers. They establish standards for response and resolution times, creating a framework for accountability and ensures a consistent, high-quality experience.",
"LEARN_MORE": "Learn more about SLA",
"COUNT": "{n} SLA | {n} SLAs",
"LOADING": "Fetching SLAs",
"SEARCH_PLACEHOLDER": "Search SLA...",
"SEARCH": {
"NO_RESULTS": "No SLA found matching your search"
},
"PAYWALL": {
"TITLE": "Upgrade to create SLAs",
"AVAILABLE_ON": "The SLA feature is only available in the Business and Enterprise plans.",
@@ -25,18 +20,14 @@
},
"LIST": {
"404": "There are no SLAs available in this account.",
"TABLE_HEADER": {
"SLA": "SLA",
"BUSINESS_HOURS": "Business hours"
},
"EMPTY": {
"TITLE_1": "Enterprise P0",
"DESC_1": "Issues raised by enterprise customers, that require immediate attention.",
"TITLE_2": "Enterprise P1",
"DESC_2": "Issues raised by enterprise customers, that needs to be acknowledged quickly."
},
"BUSINESS_HOURS_ON": "Turned on",
"BUSINESS_HOURS_OFF": "Turned off",
"BUSINESS_HOURS_ON": "Business hours on",
"BUSINESS_HOURS_OFF": "Business hours off",
"RESPONSE_TYPES": {
"FRT": "First response time threshold",
"NRT": "Next response time threshold",
@@ -1,72 +0,0 @@
{
"SNOOZE_PARSER": {
"UNITS": {
"MINUTE": "minute",
"MINUTES": "minutes",
"HOUR": "hour",
"HOURS": "hours",
"DAY": "day",
"DAYS": "days",
"WEEK": "week",
"WEEKS": "weeks",
"MONTH": "month",
"MONTHS": "months",
"YEAR": "year",
"YEARS": "years"
},
"HALF": "half",
"NEXT": "next",
"THIS": "this",
"AT": "at",
"IN": "in",
"FROM_NOW": "from now",
"NEXT_YEAR": "next year",
"MERIDIEM": {
"AM": "am",
"PM": "pm"
},
"RELATIVE": {
"TOMORROW": "tomorrow",
"DAY_AFTER_TOMORROW": "day after tomorrow",
"NEXT_WEEK": "next week",
"NEXT_MONTH": "next month",
"THIS_WEEKEND": "this weekend",
"NEXT_WEEKEND": "next weekend"
},
"TIME_OF_DAY": {
"MORNING": "morning",
"AFTERNOON": "afternoon",
"EVENING": "evening",
"NIGHT": "night",
"NOON": "noon",
"MIDNIGHT": "midnight"
},
"WORD_NUMBERS": {
"ONE": "one",
"TWO": "two",
"THREE": "three",
"FOUR": "four",
"FIVE": "five",
"SIX": "six",
"SEVEN": "seven",
"EIGHT": "eight",
"NINE": "nine",
"TEN": "ten",
"TWELVE": "twelve",
"FIFTEEN": "fifteen",
"TWENTY": "twenty",
"THIRTY": "thirty"
},
"ORDINALS": {
"FIRST": "first",
"SECOND": "second",
"THIRD": "third",
"FOURTH": "fourth",
"FIFTH": "fifth"
},
"OF": "of",
"AFTER": "after",
"WEEK": "week",
"DAY": "day"
}
}
@@ -5,9 +5,6 @@
"LOADING": "Fetching teams",
"DESCRIPTION": "Teams allow you to organize agents into groups based on their responsibilities. An agent can belong to multiple teams. When working collaboratively, you can assign conversations to specific teams.",
"LEARN_MORE": "Learn more about teams",
"COUNT": "{n} team | {n} teams",
"SEARCH_PLACEHOLDER": "Search teams...",
"NO_RESULTS": "No teams found matching your search",
"LIST": {
"404": "There are no teams created on this account.",
"EDIT_TEAM": "Edit team",
@@ -67,8 +64,8 @@
"ERROR_MESSAGE": "Couldn't save the team details. Try again."
},
"AGENTS": {
"AGENT": "Agent",
"EMAIL": "Email",
"AGENT": "AGENT",
"EMAIL": "EMAIL",
"BUTTON_TEXT": "Add agents",
"ADD_AGENTS": "Adding Agents to your Team...",
"SELECT": "select",
@@ -4,9 +4,6 @@
"LOADING_EDITOR": "جار جلب المحرر...",
"DESCRIPTION": "Agent Bots are like the most fabulous members of your team. They can handle the small stuff, so you can focus on the stuff that matters. Give them a try. You can manage your bots from this page or create new ones using the 'Add Bot' button.",
"LEARN_MORE": "Learn about agent bots",
"COUNT": "{n} bot | {n} bots",
"SEARCH_PLACEHOLDER": "Search bots...",
"NO_RESULTS": "No bots found matching your search",
"GLOBAL_BOT": "System bot",
"GLOBAL_BOT_BADGE": "النظام",
"AVATAR": {
@@ -37,8 +34,7 @@
"LOADING": "جار جلب الروبوتات...",
"TABLE_HEADER": {
"DETAILS": "Bot Details",
"URL": "رابط Webhook",
"ACTIONS": "الإجراءات"
"URL": "رابط Webhook"
}
},
"DELETE": {
@@ -9,7 +9,6 @@
"ADMINISTRATOR": "المدير",
"AGENT": "وكيل الدعم"
},
"COUNT": "{n} وكيل | {n} وكلاء",
"LIST": {
"404": "لا يوجد وكلاء دعم مرتبطين بهذا الحساب",
"TITLE": "إدارة وكلاء الدعم في فريقك",
@@ -97,8 +96,6 @@
"ERROR_MESSAGE": "تعذر الاتصال بالخادم، الرجاء المحاولة مرة أخرى لاحقاً"
}
},
"SEARCH_PLACEHOLDER": "البحث عن وكلاء...",
"NO_RESULTS": "No agents found matching your search",
"SEARCH": {
"NO_RESULTS": "لم يتم العثور على النتائج."
},
@@ -5,9 +5,6 @@
"LOADING": "جلب الصفات المخصصة",
"DESCRIPTION": "سمة مخصصة تتتبع تفاصيل إضافية حول جهات الاتصال أو المحادثات الخاصة بك - مثل خطة الاشتراك أو تاريخ الشراء الأول. يمكنك إضافة أنواع مختلفة من السمات المخصصة، مثل النص أو القوائم أو الأرقام، لالتقاط المعلومات المحددة التي تحتاجها.",
"LEARN_MORE": "تعرف على المزيد حول السمات المخصصة",
"COUNT": "{n} attribute | {n} attributes",
"SEARCH_PLACEHOLDER": "البحث عن صفات...",
"NO_RESULTS": "No attributes found matching your search",
"ATTRIBUTE_MODELS": {
"CONVERSATION": "المحادثات",
"CONTACT": "جهات الاتصال"
@@ -66,10 +63,6 @@
},
"ENABLE_REGEX": {
"LABEL": "تمكين التحقق من صحة regex"
},
"BADGES": {
"PRE_CHAT": "Pre-chat",
"RESOLUTION": "Resolution"
}
},
"API": {
@@ -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}",
"EDIT": {
"SELF": "{agentName} غير {attributes} الخاصة به إلى {values}",
"OTHER": "{agentName} غير {attributes} لـ {user} إلى {values}",
"DELETED": "{agentName} غير {attributes} للمستخدم المحذوف إلى {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} قام بتسجيل الخروج"
},
"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})"
},
"CONVERSATION": {
"DELETE": "{agentName} deleted conversation #{id}"
@@ -3,11 +3,8 @@
"HEADER": "الأتمتة",
"DESCRIPTION": "ويمكن للأتمتة أن تحل محل وتبسط العمليات القائمة التي تتطلب جهداً يدوياً، مثل إضافة تسميات وتعيين المحادثات إلى أنسب وكيل. ويسمح ذلك للفريق بالتركيز على مواطن قوتهم مع تقليل الوقت الذي يقضيه في المهام الروتينية.",
"LEARN_MORE": "تعلم المزيد عن الأتمتة",
"COUNT": "{n} automation | {n} automations",
"HEADER_BTN_TXT": "Create Automation",
"HEADER_BTN_TXT": "إضافة قاعدة أتمتة",
"LOADING": "جلب قواعد الأتمتة",
"SEARCH_PLACEHOLDER": "Search automation rules...",
"NO_RESULTS": "No automation rules found matching your search",
"ADD": {
"TITLE": "إضافة قاعدة أتمتة",
"SUBMIT": "إنشاء",
@@ -45,9 +42,9 @@
"LIST": {
"TABLE_HEADER": {
"NAME": "الاسم",
"DESCRIPTION": "الوصف",
"ACTIVE": "مفعل",
"CREATED_ON": "تم إنشاؤها في",
"ACTIONS": "الإجراءات"
"CREATED_ON": "تم إنشاؤها في"
},
"404": "لم يتم العثور على قواعد أتمتة"
},
@@ -153,8 +150,7 @@
"ADD_PRIVATE_NOTE": "Add a Private Note",
"CHANGE_PRIORITY": "تغيير الأولوية",
"ADD_SLA": "Add SLA",
"OPEN_CONVERSATION": "فتح المحادثة",
"PENDING_CONVERSATION": "تحديد المحادثة كمعلقة"
"OPEN_CONVERSATION": "فتح المحادثة"
},
"MESSAGE_TYPES": {
"INCOMING": "Incoming Message",
@@ -22,10 +22,6 @@
"UPDATE_SUCCESFUL": "تم تحديث حالة المحادثة بنجاح.",
"UPDATE_FAILED": "فشل تحديث المحادثات، الرجاء المحاولة مرة أخرى."
},
"RESOLVE": {
"ALL_MISSING_ATTRIBUTES": "لا يمكن حل المحادثات بسبب عدم وجود السمات المطلوبة",
"PARTIAL_SUCCESS": "بعض المحادثات تحتاج إلى سمات مطلوبة قبل الحل وتم تخطيها"
},
"LABELS": {
"ASSIGN_LABELS": "إضافة وسم",
"NO_LABELS_FOUND": "لم يتم العثور على تصنيفات",
@@ -3,11 +3,8 @@
"HEADER": "الردود الجاهزة",
"LEARN_MORE": "معرفة المزيد عن الاستجابات المعلبة",
"DESCRIPTION": "الردود المسبقة هي قوالب رد مكتوبة مسبقاً تساعدك على الرد بسرعة على محادثة. يمكن للوكلاء كتابة حرف '/' يتبعه الرمز المختصر لإدراج رد مسبق أثناء محادثة. ",
"COUNT": "{n} canned response | {n} canned responses",
"HEADER_BTN_TXT": "إضافة رد جاهز",
"LOADING": "جاري جلب الردود الجاهزة...",
"SEARCH_PLACEHOLDER": "Search canned responses...",
"NO_RESULTS": "No canned responses found matching your search",
"SEARCH_404": "لا توجد عناصر مطابقة لهذا الاستعلام.",
"LIST": {
"404": "لا توجد ردود جاهزة متوفرة في هذا الحساب.",
@@ -76,9 +76,6 @@
},
"waiting_since_desc": {
"TEXT": "الرد المعلق: الأقصر أولاً"
},
"priority_desc_created_at_asc": {
"TEXT": "Priority: Highest first, Created: Oldest first"
}
},
"ATTACHMENTS": {
@@ -107,7 +104,7 @@
"CONTENT": "Shared contact"
},
"embed": {
"CONTENT": "المحتوى المضمن"
"CONTENT": "Embedded content"
}
},
"CHAT_SORT_BY_FILTER": {
@@ -1,33 +1,33 @@
{
"COMPANIES": {
"HEADER": "الشركات",
"HEADER": "Companies",
"SORT_BY": {
"LABEL": "ترتيب حسب",
"OPTIONS": {
"NAME": "الاسم",
"DOMAIN": "النطاق",
"CREATED_AT": "تم إنشاؤها في",
"CONTACTS_COUNT": "عدد جهات الاتصال"
"CONTACTS_COUNT": "Contacts count"
}
},
"ORDER": {
"LABEL": "ترتيب",
"LABEL": "Order",
"OPTIONS": {
"ASCENDING": "تصاعدي",
"ASCENDING": "Ascending",
"DESCENDING": "Descending"
}
},
"SEARCH_PLACEHOLDER": "البحث في الشركات...",
"LOADING": "جاري تحميل الشركات...",
"UNNAMED": "شركة بلا اسم",
"CONTACTS_COUNT": "جهة اتصال {n} | {n} جهات الاتصال",
"SEARCH_PLACEHOLDER": "Search companies...",
"LOADING": "Loading companies...",
"UNNAMED": "Unnamed Company",
"CONTACTS_COUNT": "{n} contact | {n} contacts",
"EMPTY_STATE": {
"TITLE": "لم يتم العثور على شركات"
"TITLE": "No companies found"
}
},
"COMPANIES_LAYOUT": {
"PAGINATION_FOOTER": {
"SHOWING": "عرض {startItem} - {endItem} من {totalItems} شركة | عرض {startItem} {endItem} من الشركات {totalItems}"
"SHOWING": "Showing {startItem} {endItem} of {totalItems} company | Showing {startItem} {endItem} of {totalItems} companies"
}
}
}
@@ -18,8 +18,8 @@
"CREATED_AT_LABEL": "تم إنشاؤها",
"NEW_MESSAGE": "رسالة جديدة",
"CALL": "Call",
"CALL_INITIATED": "جار الاتصال بجهة الاتصال…",
"CALL_FAILED": "تعذر بدء المكالمة. الرجاء المحاولة مرة أخرى.",
"CALL_INITIATED": "Calling the contact…",
"CALL_FAILED": "Unable to start the call. Please try again.",
"VOICE_INBOX_PICKER": {
"TITLE": "Choose a voice inbox"
},
@@ -457,11 +457,8 @@
"INSTAGRAM": {
"PLACEHOLDER": "Add Instagram"
},
"TELEGRAM": {
"PLACEHOLDER": "Add Telegram"
},
"TIKTOK": {
"PLACEHOLDER": "إضافة TikTok"
"PLACEHOLDER": "Add TikTok"
},
"LINKEDIN": {
"PLACEHOLDER": "Add LinkedIn"
@@ -576,8 +573,7 @@
"SEARCH_EMPTY_STATE_TITLE": "لا توجد جهات اتصال تطابق بحثك 🔍",
"LIST_EMPTY_STATE_TITLE": "No contacts available in this view 📋",
"ACTIVE_EMPTY_STATE_TITLE": "No contacts are active at the moment 🌙"
},
"LOAD_MORE": "تحميل المزيد"
}
},
"CONTACTS_BULK_ACTIONS": {
"ASSIGN_LABELS": "تعيين التسميات",
@@ -611,7 +607,7 @@
"NO_INBOX_ALERT": "There are no available inboxes to start a conversation with this contact.",
"CONTACT_SELECTOR": {
"LABEL": "إلى:",
"TAG_INPUT_PLACEHOLDER": "Enter at least 2 characters to search by name, email, or phone number",
"TAG_INPUT_PLACEHOLDER": "Search for a contact with name, email or phone number",
"CONTACT_CREATING": "Creating contact..."
},
"INBOX_SELECTOR": {
@@ -622,9 +618,9 @@
"SUBJECT_LABEL": "الموضوع :",
"SUBJECT_PLACEHOLDER": "Enter your email subject here",
"CC_LABEL": "نسخة من البريد:",
"CC_PLACEHOLDER": "Enter at least 2 characters to search by email",
"CC_PLACEHOLDER": "Search for a contact with their email address",
"BCC_LABEL": "نسخة خفية من البريد:",
"BCC_PLACEHOLDER": "Enter at least 2 characters to search by email",
"BCC_PLACEHOLDER": "Search for a contact with their email address",
"BCC_BUTTON": "نسخة خفية من البريد"
},
"MESSAGE_EDITOR": {
@@ -61,7 +61,6 @@
"UNSUPPORTED_MESSAGE": "This message is unsupported. You can view this message on the Facebook / Instagram app.",
"UNSUPPORTED_MESSAGE_FACEBOOK": "هذه الرسالة غير مدعومة، يمكنك مشاهدة هذه الرسالة على تطبيق فيسبوك (Messenger).",
"UNSUPPORTED_MESSAGE_INSTAGRAM": "هذه الرسالة غير مدعومة، يمكنك عرض هذه الرسالة على تطبيق Instagram.",
"UNSUPPORTED_MESSAGE_TIKTOK": "هذه الرسالة غير مدعومة. يمكنك مشاهدة هذه الرسالة على تطبيق TikTok.",
"SUCCESS_DELETE_MESSAGE": "تم حذف الرسالة بنجاح",
"FAIL_DELETE_MESSSAGE": "تعذر حذف الرسالة! حاول مرة أخرى",
"NO_RESPONSE": "لا توجد استجابة",
@@ -171,13 +170,9 @@
"FAILED": "تعذر تعيين الوكيل. الرجاء المحاولة مرة أخرى."
},
"LABEL_ASSIGNMENT": {
"SUCCESFUL": "تعيين تسمية #{labelName} لمعرف المحادثة {conversationId}",
"SUCCESFUL": "تعيين تسمية ##{labelName} لمعرف المحادثة {conversationId}",
"FAILED": "تعذر تعيين التسمية. الرجاء المحاولة مرة أخرى."
},
"LABEL_REMOVAL": {
"SUCCESFUL": "Removed label #{labelName} from conversation id {conversationId}",
"FAILED": "Couldn't remove label. Please try again."
},
"TEAM_ASSIGNMENT": {
"SUCCESFUL": "الفريق المعين \"{team}\" لمعرف المحادثة {conversationId}",
"FAILED": "تعذر تعيين الفريق. الرجاء المحاولة مرة أخرى."
@@ -190,11 +185,7 @@
"DISABLE_SIGN_TOOLTIP": "تعطيل التوقيع",
"MSG_INPUT": "زر Shift + Enter لإضافة سطر جديد. ابدأ بزر / للاختيار من الردود الجاهزة.",
"PRIVATE_MSG_INPUT": "زر Shift + Enter لإضافة سطر جديد. سيكون هذا مرئياً للوكلاء فقط",
"MESSAGING_RESTRICTED": "You cannot reply to this conversation",
"MESSAGING_RESTRICTED_WHATSAPP": "You can only reply using a template message due to 24-hour message window restriction",
"MESSAGING_RESTRICTED_API": "You can only reply using a template message due to message window restriction",
"MESSAGE_SIGNATURE_NOT_CONFIGURED": "لم يتم تكوين توقيع الرسالة، الرجاء تكوينه في إعدادات الملف الشخصي.",
"COPILOT_MSG_INPUT": "إعطاء copilot أوامر إضافية، أو السؤال عن أي شيء آخر... اضغط على مفتاح الإدخال لإرسال المتابعة",
"CLICK_HERE": "انقر هنا للتحديث",
"WHATSAPP_TEMPLATES": "قوالب الواتساب"
},
@@ -214,7 +205,7 @@
"DRAG_DROP": "اسحب و أسقط هنا للإرفاق",
"START_AUDIO_RECORDING": "بدء التسجيل الصوتي",
"STOP_AUDIO_RECORDING": "إيقاف التسجيل الصوتي",
"COPILOT_THINKING": "Copilot يفكر",
"": "",
"EMAIL_HEAD": {
"TO": "إلى",
"ADD_BCC": "إضافة bcc",
@@ -256,12 +247,9 @@
"SUCCESS_DELETE_CONVERSATION": "Conversation deleted successfully",
"FAIL_DELETE_CONVERSATION": "Couldn't delete conversation! Try again",
"FILE_SIZE_LIMIT": "حجم الملف يتجاوز حد الاقصى وهو {MAXIMUM_SUPPORTED_FILE_UPLOAD_SIZE}",
"FILE_TYPE_NOT_SUPPORTED": "هذا النوع من الملفات {fileName} غير مدعوم في هذه المحادثة",
"MESSAGE_ERROR": "غير قادر على إرسال هذه الرسالة، الرجاء المحاولة مرة أخرى لاحقاً",
"SENT_BY": "أرسلت بواسطة:",
"BOT": "رد آلي",
"NATIVE_APP": "تطبيق الجوال",
"NATIVE_APP_ADVISORY": "تم إرسال هذه الرسالة من تطبيق الجوال. رد من Chatwoot للحفاظ على نافذة الرسالة.",
"SEND_FAILED": "تعذر إرسال الرسالة! حاول مرة أخرى",
"TRY_AGAIN": "إعادة المحاولة",
"ASSIGNMENT": {
@@ -289,14 +277,14 @@
"COPILOT": "Copilot"
},
"VOICE_WIDGET": {
"INCOMING_CALL": "مكالمة واردة",
"OUTGOING_CALL": "مكالمة صادرة",
"CALL_IN_PROGRESS": "مكالمة قيد الاتصال",
"NOT_ANSWERED_YET": "لم يتم الرد بعد",
"HANDLED_IN_ANOTHER_TAB": "يتم التعامل معها في علامة تبويب أخرى",
"REJECT_CALL": "رفض",
"JOIN_CALL": "انضم إلى المكالمة",
"END_CALL": "إنهاء المكالمة"
"INCOMING_CALL": "Incoming call",
"OUTGOING_CALL": "Outgoing call",
"CALL_IN_PROGRESS": "Call in progress",
"NOT_ANSWERED_YET": "Not answered yet",
"HANDLED_IN_ANOTHER_TAB": "Being handled in another tab",
"REJECT_CALL": "Reject",
"JOIN_CALL": "Join call",
"END_CALL": "End call"
}
},
"EMAIL_TRANSCRIPT": {
@@ -306,7 +294,6 @@
"CANCEL": "إلغاء",
"SEND_EMAIL_SUCCESS": "تم إرسال نص المحادثة بنجاح",
"SEND_EMAIL_ERROR": "حدث خطأ، الرجاء المحاولة مرة أخرى",
"SEND_EMAIL_PAYMENT_REQUIRED": "Email transcript is not available on your current plan. Please upgrade to use this feature.",
"FORM": {
"SEND_TO_CONTACT": "إرسال نص المحادثة إلى العميل",
"SEND_TO_AGENT": "إرسال نص المحادثة إلى وكيل خدمة العملاء المعين",
@@ -3,11 +3,8 @@
"HEADER": "Custom Roles",
"LEARN_MORE": "Learn more about custom roles",
"DESCRIPTION": "Custom roles are roles that are created by the account owner or admin. These roles can be assigned to agents to define their access and permissions within the account. Custom roles can be created with specific permissions and access levels to suit the requirements of the organization.",
"COUNT": "{n} custom role | {n} custom roles",
"HEADER_BTN_TXT": "Add custom role",
"LOADING": "Fetching custom roles...",
"SEARCH_PLACEHOLDER": "Search custom roles...",
"NO_RESULTS": "No custom roles found matching your search",
"SEARCH_404": "لا توجد عناصر مطابقة لهذا الاستعلام.",
"PAYWALL": {
"TITLE": "Upgrade to create custom roles",
@@ -1,8 +1,5 @@
{
"DATE_PICKER": {
"PREVIOUS_PERIOD": "Previous period",
"NEXT_PERIOD": "Next period",
"WEEK_NUMBER": "Week #{weekNumber}",
"APPLY_BUTTON": "تطبيق",
"CLEAR_BUTTON": "مسح",
"DATE_RANGE_INPUT": {
@@ -16,8 +13,6 @@
"LAST_3_MONTHS": "آخر 3 أشهر",
"LAST_6_MONTHS": "آخر 6 أشهر",
"LAST_YEAR": "العام الماضي",
"THIS_WEEK": "This week",
"MONTH_TO_DATE": "This month",
"CUSTOM_RANGE": "تحديد نطاق التاريخ"
}
}
@@ -7,13 +7,6 @@
},
"CLOSE": "أغلق",
"BETA": "تجريبي",
"BETA_DESCRIPTION": "This feature is in beta and may change as we improve it.",
"ACCEPT": "قبول",
"DISCARD": "Discard",
"PREFERRED": "المفضلة"
},
"CHOICE_TOGGLE": {
"YES": "نعم",
"NO": "لا"
"BETA_DESCRIPTION": "This feature is in beta and may change as we improve it."
}
}
@@ -182,7 +182,6 @@
},
"COMMAND_BAR": {
"SEARCH_PLACEHOLDER": "البحث أو القفز إلى",
"SNOOZE_PLACEHOLDER": "Type a time e.g. tomorrow, 2 hours, next friday, jan 15...",
"SECTIONS": {
"GENERAL": "عام",
"REPORTS": "التقارير",
@@ -374,16 +374,6 @@
"ERROR_MESSAGE": "Error while deleting article"
}
},
"REORDER_ARTICLE": {
"API": {
"ERROR_MESSAGE": "Unable to reorder articles. Please try again."
}
},
"REORDER_CATEGORY": {
"API": {
"ERROR_MESSAGE": "Unable to reorder categories. Please try again."
}
},
"CREATE_ARTICLE": {
"ERROR_MESSAGE": "Please add the article heading and content then only you can update the settings"
},
@@ -3,9 +3,6 @@
"HEADER": "قنوات التواصل",
"DESCRIPTION": "القناة هي وضع الاتصال الذي يختاره العميل للتفاعل معك. صندوق الوارد هو المكان الذي تدير فيه التفاعلات لقناة معينة. ويمكن أن تشمل الاتصالات من مصادر مختلفة مثل البريد الإلكتروني، والمحادثة الحية، ووسائط الإعلام الاجتماعية.",
"LEARN_MORE": "تعلم المزيد عن صناديق البريد",
"COUNT": "{n} inbox | {n} inboxes",
"SEARCH_PLACEHOLDER": "Search inboxes...",
"NO_RESULTS": "No inboxes found matching your search",
"RECONNECTION_REQUIRED": "Your inbox is disconnected. You won't receive new messages until you reauthorize it.",
"CLICK_TO_RECONNECT": "Click here to reconnect.",
"WHATSAPP_REGISTRATION_INCOMPLETE": "Your WhatsApp Business registration isnt complete. Please check your display name status in Meta Business Manager before reconnecting.",
@@ -61,11 +58,11 @@
"DUPLICATE_INBOX_BANNER": "This Instagram account was migrated to the new Instagram channel inbox. You wont be able to send/receive Instagram messages from this inbox anymore."
},
"TIKTOK": {
"CONTINUE_WITH_TIKTOK": "المتابعة مع تيكتوك",
"CONNECT_YOUR_TIKTOK_PROFILE": "الاتصال بحسابك في تيكتوك",
"HELP": "لإضافة ملفك الشخصي على TikTok كقناة، عليك مصادقة ملفك الشخصي على TikTok بالنقر على \"متابعة مع TikTok\".",
"ERROR_MESSAGE": "حدث خطأ أثناء الاتصال بـ TikTok، يرجى المحاولة مرة أخرى",
"ERROR_AUTH": "حدث خطأ أثناء الاتصال بـ TikTok، يرجى المحاولة مرة أخرى"
"CONTINUE_WITH_TIKTOK": "Continue with TikTok",
"CONNECT_YOUR_TIKTOK_PROFILE": "Connect your TikTok Profile",
"HELP": "To add your TikTok profile as a channel, you need to authenticate your TikTok Profile by clicking on 'Continue with TikTok' ",
"ERROR_MESSAGE": "There was an error connecting to TikTok, please try again",
"ERROR_AUTH": "There was an error connecting to TikTok, please try again"
},
"TWITTER": {
"HELP": "لإضافة حساب تويتر الخاص بك كقناة تواصل، تحتاج إلى مصادقة حسابك على تويتر بك بالنقر على زر \"تسجيل الدخول باستخدام تويتر\" ",
@@ -392,10 +389,10 @@
"ERROR_MESSAGE": "لم نتمكن من حفظ قناة البريد الإلكتروني"
},
"FINISH_MESSAGE": "بدء إعادة توجيه رسائل البريد الإلكتروني الخاصة بك إلى عنوان البريد الإلكتروني التالي.",
"FINISH_MESSAGE_NO_FORWARDING": "تم إنشاء بريدك الإلكتروني بنجاح! تحتاج إلى تكوين بيانات اعتماد SMTP و IMAP لإرسال واستقبال رسائل البريد الإلكتروني. بدون هذه الإعدادات، لن يتم معالجة رسائل البريد الإلكتروني.",
"FORWARDING_ADDRESS_LABEL": "إعادة توجيه رسائل البريد الإلكتروني إلى هذا العنوان",
"FINISH_MESSAGE_NO_FORWARDING": "Your email inbox has been created successfully! You need to configure SMTP and IMAP credentials to send and receive emails. Without these settings, no emails will be processed.",
"FORWARDING_ADDRESS_LABEL": "Forward emails to this address:",
"CONFIGURE_SMTP_IMAP_LINK": "اضغط هنا",
"CONFIGURE_SMTP_IMAP_TEXT": " لتهيئة إعدادات IMAP و SMTP"
"CONFIGURE_SMTP_IMAP_TEXT": " to configure IMAP and SMTP settings"
},
"LINE_CHANNEL": {
"TITLE": "قناة LINE",
@@ -483,7 +480,7 @@
},
"TIKTOK": {
"TITLE": "TikTok",
"DESCRIPTION": "ربط حسابك في TikTok"
"DESCRIPTION": "Connect your TikTok account"
},
"VOICE": {
"TITLE": "Voice",
@@ -578,7 +575,7 @@
"SUBTITLE": "Use only the configured business name as the sender name in the email header."
},
"BUSINESS_NAME": {
"BUTTON_TEXT": "Configure your business name",
"BUTTON_TEXT": "+ Configure your business name",
"PLACEHOLDER": "Enter your business name",
"SAVE_BUTTON_TEXT": "حفظ"
}
@@ -592,10 +589,8 @@
"DISABLED": "معطّل"
},
"LOCK_TO_SINGLE_CONVERSATION": {
"ENABLED": "Reopen same conversation",
"DISABLED": "Create new conversations",
"ENABLED_DESCRIPTION": "When a contact messages again, the previous conversation will be reopened.",
"DISABLED_DESCRIPTION": "A new conversation will be created each time after the previous one is resolved."
"ENABLED": "مفعل",
"DISABLED": "معطّل"
},
"ENABLE_HMAC": {
"LABEL": "تفعيل"
@@ -630,8 +625,6 @@
"ACCOUNT_HEALTH": "Account Health",
"CSAT": "تقييم رضاء العملاء"
},
"CHANNEL_PREFERENCES": "Channel Preferences",
"WIDGET_FEATURES": "Widget features",
"ACCOUNT_HEALTH": {
"TITLE": "Manage your WhatsApp account",
"DESCRIPTION": "Review your WhatsApp account status, messaging limits, and quality. Update settings or resolve issues if needed",
@@ -685,16 +678,6 @@
"SANDBOX": "Sandbox",
"LIVE": "مباشر"
}
},
"WEBHOOK": {
"TITLE": "Webhook Configuration",
"DESCRIPTION": "Webhook URL is required for your WhatsApp Business Account to receive messages from customers",
"ACTION_REQUIRED": "Webhook not configured",
"REGISTER_BUTTON": "Register Webhook",
"REGISTER_SUCCESS": "Webhook registered successfully",
"REGISTER_ERROR": "Failed to register webhook. Please try again.",
"CONFIGURED_SUCCESS": "Webhook configured successfully",
"URL_MISMATCH": "Webhook URL mismatch"
}
},
"SETTINGS": "الإعدادات",
@@ -710,20 +693,8 @@
"MESSENGER_SUB_HEAD": "ضع هذا الكود داخل وسم الـ body في موقعك",
"ALLOWED_DOMAINS": {
"TITLE": "Allowed Domains",
"DESCRIPTION": "Restrict which websites can embed your chat widget. For security, only add domains you own and trust. Add one or more domains separated by commas. Leave blank to allow all domains (not recommended for production).",
"PLACEHOLDER": "example.com, www.example.com, app.example.com"
},
"ALLOW_MOBILE_WEBVIEW": {
"LABEL": "Enable widget in mobile apps",
"SUBTITLE": "Check this if you embed the widget in iOS or Android apps. Mobile apps don't send domain information, so they would be blocked by domain restrictions unless this is enabled."
},
"IDENTITY_VALIDATION": {
"TITLE": "Identity Validation",
"DESCRIPTION": "Verify user authenticity by generating secure tokens. This prevents unauthorized users from impersonating others in your chat.",
"SECRET_KEY": "Secret Key",
"VIEW_DOCS": "View documentation",
"REQUIRE_LABEL": "Require identity validation for all conversations",
"REQUIRE_DESCRIPTION": "When enabled, users must provide a valid identity token to start conversations. Requests without valid tokens will be rejected."
"SUBTITLE": "Add wildcard or regular domains separated by commas (leave blank to allow all), e.g. *.chatwoot.dev, chatwoot.com.",
"PLACEHOLDER": "Enter domains separated by commas (eg: *.chatwoot.dev, chatwoot.com)"
},
"INBOX_AGENTS": "وكيل الدعم",
"INBOX_AGENTS_SUB_TEXT": "إضافة أو إزالة وكلاء من صندوق الوارد هذا",
@@ -737,8 +708,8 @@
"SENDER_NAME_SECTION_TEXT": "تمكين/تعطيل إظهار اسم الوكيل في البريد الإلكتروني، إذا تم تعطيله فسيظهر اسم المنشأة",
"ENABLE_CONTINUITY_VIA_EMAIL": "تمكين استمرارية المحادثة عبر البريد الإلكتروني",
"ENABLE_CONTINUITY_VIA_EMAIL_SUB_TEXT": "المحادثات ستستمر عبر البريد الإلكتروني إذا كان عنوان البريد الإلكتروني لجهة الاتصال متاحاً.",
"LOCK_TO_SINGLE_CONVERSATION": "Conversation Routing",
"LOCK_TO_SINGLE_CONVERSATION_SUB_TEXT": "Configure conversation creation for existing contacts",
"LOCK_TO_SINGLE_CONVERSATION": "قفل إلى محادثة واحدة",
"LOCK_TO_SINGLE_CONVERSATION_SUB_TEXT": "تمكين أو تعطيل محادثات متعددة لنفس جهة الاتصال في هذا البريد الوارد",
"INBOX_UPDATE_TITLE": "إعدادات قناة التواصل",
"INBOX_UPDATE_SUB_TEXT": "تحديث إعدادات قناة التواصل",
"AUTO_ASSIGNMENT_SUB_TEXT": "تمكين أو تعطيل الإسناد التلقائي للمحادثات الجديدة إلى الموظفين المضافين إلى قناة التواصل هذه.",
@@ -787,7 +758,6 @@
"LABEL": "مركز المساعدة",
"PLACEHOLDER": "Select Help Center",
"SELECT_PLACEHOLDER": "Select Help Center",
"NONE": "لا شيء",
"REMOVE": "Remove Help Center",
"SUB_TEXT": "Attach a Help Center with the inbox"
},
@@ -796,53 +766,6 @@
"MAX_ASSIGNMENT_LIMIT_RANGE_ERROR": "الرجاء إدخال قيمة أكبر من 0",
"MAX_ASSIGNMENT_LIMIT_SUB_TEXT": "تحديد الحد الأقصى لعدد المحادثات من علبة الوارد هذه التي يمكن تعيينها تلقائياً إلى وكيل"
},
"ASSIGNMENT": {
"TITLE": "تعيين المحادثة",
"DESCRIPTION": "تعيين المحادثات الواردة تلقائياً إلى الوكلاء المتاحين استناداً إلى سياسات التعيين",
"ENABLE_AUTO_ASSIGNMENT": "تمكين تعيين المحادثة تلقائياً",
"DEFAULT_RULES_TITLE": "قواعد التعيين الافتراضية",
"DEFAULT_RULES_DESCRIPTION": "استخدام سلوك التعيين الافتراضي لجميع المحادثات",
"DEFAULT_RULE_1": "المحادثات التي تم إنشاؤها أولاً",
"DEFAULT_RULE_2": "توزيع الجدولة الدائرية (Round robin)",
"CUSTOMIZE_WITH_POLICY": "Customize with assignment policy",
"USING_POLICY": "استخدام سياسة التعيين المخصصة لهذه القناة",
"CUSTOMIZE_POLICY": "التخصيص وفقًا لسياسة التعيين",
"DELETE_POLICY": "حذف سياسة",
"POLICY_LABEL": "سياسة التعيين",
"ASSIGNMENT_ORDER_LABEL": "ترتيب التعيين",
"ASSIGNMENT_METHOD_LABEL": "طريقة التعيين",
"POLICY_STATUS": {
"ACTIVE": "مفعل",
"INACTIVE": "غير نشط"
},
"PRIORITY": {
"EARLIEST_CREATED": "تم إنشاؤها في وقت سابق",
"LONGEST_WAITING": "أطول انتظار"
},
"METHOD": {
"ROUND_ROBIN": "Round robin",
"BALANCED": "تعيين متوازن"
},
"UPGRADE_PROMPT": "سياسة التعيين المخصصة متاحى في الخطة (Business) ",
"UPGRADE_TO_BUSINESS": "الترقية إلى (Business)",
"DEFAULT_POLICY_LINKED": "السياسة الافتراضية المرتبطة",
"DEFAULT_POLICY_DESCRIPTION": "ربط سياسة تعيين مخصصة لتخصيص كيفية تعيين المحادثات إلى الوكلاء في هذه القناه",
"LINK_EXISTING_POLICY": "ربط السياسة الحالية",
"CREATE_NEW_POLICY": "إنشاء سياسة جديدة",
"NO_POLICIES": "لم يتم العثور على سياسات التعيين",
"VIEW_ALL_POLICIES": "عرض جميع السياسات",
"CURRENT_BEHAVIOR": "حاليا يستخدم سلوك التعيين الافتراضي:",
"LINK_SUCCESS": "تم ربط سياسة التعيين بنجاح",
"LINK_ERROR": "فشل في ربط سياسة التعيين"
},
"ASSIGNMENT_POLICY": {
"DELETE_CONFIRM_TITLE": "حذف سياسة التعيين؟",
"DELETE_CONFIRM_MESSAGE": "هل أنت متأكد من أنك تريد إزالة سياسة التعيين هذه من صندوق الوارد هذا؟ صندوق الوارد سوف يعود إلى قواعد التعيين الافتراضية.",
"CANCEL": "إلغاء",
"CONFIRM_DELETE": "حذف",
"DELETE_SUCCESS": "تمت إزالة سياسة التعيين بنجاح",
"DELETE_ERROR": "فشل في إزالة سياسة التعيين"
},
"FACEBOOK_REAUTHORIZE": {
"TITLE": "إعادة التصريح",
"SUBTITLE": "انتهت صلاحية اتصال الفيسبوك الخاص بك، يرجى إعادة الاتصال بصفحة الفيسبوك الخاصة بك لمواصلة الخدمات",
@@ -886,48 +809,34 @@
"PLACEHOLDER": "Please enter a message to show users with the form"
},
"BUTTON_TEXT": {
"LABEL": "نص الزر",
"PLACEHOLDER": "يرجى تقييمنا"
"LABEL": "Button text",
"PLACEHOLDER": "Please rate us"
},
"LANGUAGE": {
"LABEL": "اللغة",
"PLACEHOLDER": "اختر لغة القالب"
"PLACEHOLDER": "Select template language"
},
"MESSAGE_PREVIEW": {
"LABEL": "معاينة الرسالة",
"TOOLTIP": "قد يختلف هذا قليلاً عند تقديمه على منصة WhatsApp."
"LABEL": "Message preview",
"TOOLTIP": "This may vary slightly when rendered on WhatsApp's platform."
},
"TEMPLATE_STATUS": {
"APPROVED": "معتمد بواسطة WhatsApp",
"PENDING": "في انتظار موافقة WhatsApp",
"REJECTED": "Meta رفضت القالب",
"DEFAULT": "يحتاج موافقة WhatsApp",
"NOT_FOUND": "هذا القالب غير موجود في منصة ميتا."
"APPROVED": "Approved by WhatsApp",
"PENDING": "Pending WhatsApp approval",
"REJECTED": "Meta rejected the template",
"DEFAULT": "Needs WhatsApp approval",
"NOT_FOUND": "The template does not exist in the Meta platform."
},
"TEMPLATE_CREATION": {
"SUCCESS_MESSAGE": "تم إنشاء قالب WhatsApp بنجاح وإرساله للموافقة عليه",
"ERROR_MESSAGE": "فشل إنشاء قالب WhatsApp"
"SUCCESS_MESSAGE": "WhatsApp template created successfully and sent for approval",
"ERROR_MESSAGE": "Failed to create WhatsApp template"
},
"TEMPLATE_UPDATE_DIALOG": {
"TITLE": "تعديل تفاصيل الاستبيان",
"DESCRIPTION": "سوف نقوم بحذف القالب السابق ونقوم بإنشاء قالب جديد حيث سيتم إرساله مرة أخرى للاعتماد من قبل Whatsapp",
"CONFIRM": "إنشاء قالب جديد",
"TITLE": "Edit survey details",
"DESCRIPTION": "We will delete the previous template and make a new one which will be sent again for WhatsApp approval",
"CONFIRM": "Create new template",
"CANCEL": "العودة للخلف"
},
"UTILITY_ANALYZER": {
"ACTION": "Check utility fit",
"HELPER_NOTE": "Check this message before submission to improve Utility fit. The system creates a dedicated CSAT template with buttons for reporting and submits it as Utility; Meta may still reclassify it as Marketing based on content.",
"RESULT_LABEL": "Meta category prediction",
"GUIDANCE_NOTE": "This is a guidance check, not a guarantee of Meta approval.",
"SUGGESTION_LABEL": "Suggested utility-safe rewrite",
"APPLY": "Use this rewrite",
"ERROR_MESSAGE": "Couldn't analyze the message. Please try again.",
"CLASSIFICATION": {
"LIKELY_UTILITY": "Likely Utility",
"LIKELY_MARKETING": "Likely Marketing",
"UNCLEAR": "Needs clarification"
}
},
"SURVEY_RULE": {
"LABEL": "Survey rule",
"DESCRIPTION_PREFIX": "Send the survey if the conversation",
@@ -939,7 +848,7 @@
"SELECT_PLACEHOLDER": "select labels"
},
"NOTE": "Note: CSAT surveys are sent only once per conversation",
"WHATSAPP_NOTE": "Note: When you save, the system creates a dedicated CSAT template in WhatsApp (used to capture rating and feedback in reports) and submits it as Utility for approval. Meta may still classify it as Marketing based on content. After approval, surveys are sent only once per conversation as per the survey rule.",
"WHATSAPP_NOTE": "Note: We will create a template and send it for WhatsApp approval. After being approved, surveys will be sent only once per conversation as per the survey rule.",
"API": {
"SUCCESS_MESSAGE": "CSAT settings updated successfully",
"ERROR_MESSAGE": "We couldn't update CSAT settings. Please try again later."
@@ -955,11 +864,9 @@
"UNAVAILABLE_MESSAGE_LABEL": "رسالة غير متاح للزائرين",
"TOGGLE_HELP": "تمكين توفر العمل سيظهر الساعات المتاحة على أداة الدردشة المباشرة حتى لو كان جميع الوكلاء غير متصلين بالإنترنت. خارج الساعات المتاحة يمكن تحذير الزوار برسالة ونموذج ما قبل الدردشة.",
"DAY": {
"DAY": "اليوم",
"AVAILABILITY": "التوفر",
"HOURS": "Hours",
"ENABLE": "تمكين التوفر لهذا اليوم",
"UNAVAILABLE": "غير متوفر",
"HOURS": "ساعات",
"VALIDATION_ERROR": "يجب أن يكون وقت البدء قبل وقت الإغلاق.",
"CHOOSE": "اختر"
},
@@ -1066,12 +973,11 @@
"IN_A_DAY": "خلال يوم"
},
"WIDGET_COLOR_LABEL": "لون صندوق الدردشة",
"WIDGET_BUBBLE": "Bubble",
"WIDGET_BUBBLE_POSITION_LABEL": "Position:",
"WIDGET_BUBBLE_TYPE_LABEL": "النوع:",
"WIDGET_BUBBLE_POSITION_LABEL": "موقع شعار اللايف شات",
"WIDGET_BUBBLE_TYPE_LABEL": "شكل عرض اللايف شات",
"WIDGET_BUBBLE_LAUNCHER_TITLE": {
"DEFAULT": "تحدث الينا",
"LABEL": "Launcher Title",
"LABEL": "عنوان ايقونة اللايف شات",
"PLACE_HOLDER": "تحدث الينا"
},
"UPDATE": {
@@ -1096,7 +1002,7 @@
},
"WIDGET_SCREEN": {
"DEFAULT": "افتراضي",
"CHAT": "Chat mode"
"CHAT": "محادثة"
},
"REPLY_TIME": {
"IN_A_FEW_MINUTES": "عادة نقوم بالرد خلال بضع دقائق",
@@ -3,9 +3,6 @@
"FETCHING": "جلب التكاملات",
"NO_HOOK_CONFIGURED": "لا يوجد {integrationId} تكاملات مكونة في هذا الحساب.",
"HEADER": "التطبيقات",
"COUNT": "{n} integration | {n} integrations",
"SEARCH_PLACEHOLDER": "Search...",
"NO_RESULTS": "No results found matching your search",
"STATUS": {
"ENABLED": "مفعل",
"DISABLED": "معطّل"
@@ -34,7 +31,6 @@
"LIST": {
"FETCHING": "جلب روابط التكامل",
"INBOX": "صندوق الوارد",
"ACTIONS": "الإجراءات",
"DELETE": {
"BUTTON_TEXT": "حذف"
}
@@ -1,7 +1,6 @@
{
"INTEGRATION_SETTINGS": {
"SHOPIFY": {
"HEADER": "Shopify",
"DELETE": {
"TITLE": "Delete Shopify Integration",
"MESSAGE": "Are you sure you want to delete the Shopify integration?"
@@ -20,8 +19,6 @@
"DESCRIPTION": "Chatwoot تتكامل مع أدوات وخدمات متعددة لتحسين كفاءة فريقك. استكشف القائمة أدناه لتكوين تطبيقاتك المفضلة.",
"LEARN_MORE": "معرفة المزيد عن التكاملات",
"LOADING": "جاري جلب التكاملات",
"SEARCH_PLACEHOLDER": "Search integrations...",
"NO_RESULTS": "No integrations found matching your search",
"CAPTAIN": {
"DISABLED": "لم يتم تمكين الكابتن على حسابك.",
"CLICK_HERE_TO_CONFIGURE": "انقر هنا للتهيئة",
@@ -31,17 +28,6 @@
"WEBHOOK": {
"SUBSCRIBED_EVENTS": "الأحداث المشتركة",
"LEARN_MORE": "Learn more about webhooks",
"SECRET": {
"LABEL": "Secret",
"COPY": "Copy secret to clipboard",
"COPY_SUCCESS": "Secret copied to clipboard",
"TOGGLE": "Toggle secret visibility",
"CREATED_DESC": "Your webhook has been created. Use the secret below to verify webhook signatures. Please copy it now — you can also find it later in the webhook edit form.",
"DONE": "Done"
},
"COUNT": "{n} webhook | {n} webhooks",
"SEARCH_PLACEHOLDER": "Search webhooks...",
"NO_RESULTS": "No webhooks found matching your search",
"FORM": {
"CANCEL": "إلغاء",
"DESC": "أحداث Webhook توفر لك معلومات في الوقت الحقيقي حول ما يحدث في حساب Chatwoot الخاص بك. الرجاء إدخال عنوان URL صالح لتكوين callback.",
@@ -118,7 +104,6 @@
}
},
"SLACK": {
"HEADER": "Slack",
"DELETE": "حذف",
"DELETE_CONFIRMATION": {
"TITLE": "Delete the integration",
@@ -160,29 +145,7 @@
"EXPAND": "Expand",
"MAKE_FRIENDLY": "Change message tone to friendly",
"MAKE_FORMAL": "Use formal tone",
"SIMPLIFY": "تبسيط",
"CONFIDENT": "استخدام نبرة لطيفة",
"PROFESSIONAL": "استخدام نبرة احترافية",
"CASUAL": "استخدم نبرة عادية",
"STRAIGHTFORWARD": "استخدام نبرة مباشرة"
},
"REPLY_OPTIONS": {
"IMPROVE_REPLY": "تحسين الرد",
"IMPROVE_REPLY_SELECTION": "تحسين عملية الاختيار",
"CHANGE_TONE": {
"TITLE": "تغيير النبرة",
"OPTIONS": {
"PROFESSIONAL": "مهني",
"CASUAL": "عادية",
"STRAIGHTFORWARD": "مباشر",
"CONFIDENT": "لطيفة",
"FRIENDLY": "ودي"
}
},
"GRAMMAR": "أصلاح القواعد النحوية والإملائية",
"SUGGESTION": "اقترح رداً",
"SUMMARIZE": "تلخيص المحادثة",
"ASK_COPILOT": "إسأل المساعد"
"SIMPLIFY": "تبسيط"
},
"ASSISTANCE_MODAL": {
"DRAFT_TITLE": "Draft content",
@@ -238,16 +201,12 @@
"SIDEBAR_TXT": "<p><b>Dashboard Apps</b></p><p>Dashboard Apps allow organizations to embed an application inside the Chatwoot dashboard to provide the context for customer support agents. This feature allows you to create an application independently and embed that inside the dashboard to provide user information, their orders, or their previous payment history.</p><p>When you embed your application using the dashboard in Chatwoot, your application will get the context of the conversation and contact as a window event. Implement a listener for the message event on your page to receive the context.</p><p>To add a new dashboard app, click on the button 'Add a new dashboard app'.</p>",
"DESCRIPTION": "تسمح تطبيقات لوحة التحكم للمنظمات بتضمين تطبيق داخل لوحة التحكم لتوفير السياق لوكلاء دعم العملاء. هذه الميزة تسمح لك بإنشاء تطبيق بشكل مستقل وإدراج لتوفير معلومات المستخدم أو طلباتهم أو سجل الدفع السابق.",
"LEARN_MORE": "معرفة المزيد حول تطبيقات لوحة التحكم",
"COUNT": "{n} dashboard app | {n} dashboard apps",
"SEARCH_PLACEHOLDER": "Search dashboard apps...",
"NO_RESULTS": "No dashboard apps found matching your search",
"LIST": {
"404": "لا توجد تطبيقات لوحة التحكم التي تم تكوينها على هذا الحساب حتى الآن",
"LOADING": "جلب تطبيقات لوحة التحكم...",
"TABLE_HEADER": {
"NAME": "الاسم",
"ENDPOINT": "نقطة الوصول",
"ACTIONS": "الإجراءات"
"ENDPOINT": "نقطة الوصول"
},
"EDIT_TOOLTIP": "تعديل التطبيق",
"DELETE_TOOLTIP": "حذف التطبيق"
@@ -284,7 +243,6 @@
}
},
"LINEAR": {
"HEADER": "Linear",
"ADD_OR_LINK_BUTTON": "Create/Link Linear Issue",
"LOADING": "جلب مشاكل من Linear...",
"LOADING_ERROR": "حدث خطأ أثناء جلب المشكلات من Linear، الرجاء المحاولة مرة أخرى",
@@ -379,7 +337,6 @@
}
},
"NOTION": {
"HEADER": "نوشن",
"DELETE": {
"TITLE": "Are you sure you want to delete the Notion integration?",
"MESSAGE": "Deleting this integration will remove access to your Notion workspace and stop all related functionality.",
@@ -449,7 +406,6 @@
"CANCEL_ANYTIME": "You can change or cancel your plan anytime"
},
"ENTERPRISE_PAYWALL": {
"AVAILABLE_ON": "ولا يتوفر الكابتن AI إلا في خطط المؤسسة.",
"UPGRADE_PROMPT": "Upgrade your plan to get access to our assistants, copilot and more.",
"ASK_ADMIN": "Please reach out to your administrator for the upgrade."
},
@@ -527,8 +483,7 @@
"TITLE": "الخصائص",
"ALLOW_CONVERSATION_FAQS": "Generate FAQs from resolved conversations",
"ALLOW_MEMORIES": "Capture key details as memories from customer interactions.",
"ALLOW_CITATIONS": "Include source citations in responses",
"ALLOW_CONTACT_ATTRIBUTES": "Allow access to contact information"
"ALLOW_CITATIONS": "Include source citations in responses"
}
},
"EDIT": {
@@ -5,9 +5,6 @@
"LOADING": "جار جلب الوسوم",
"DESCRIPTION": "Labels help you categorize and prioritize conversations and leads. You can assign a label to a conversation or contact using the side panel.",
"LEARN_MORE": "معرفة المزيد حول التسميات",
"COUNT": "{n} label | {n} labels",
"SEARCH_PLACEHOLDER": "ابحث عن تصنيفات...",
"NO_RESULTS": "No labels found matching your search",
"SEARCH_404": "لا توجد عناصر مطابقة لهذا الاستعلام",
"LIST": {
"404": "لا توجد وسوم متوفرة في هذا الحساب.",
@@ -16,8 +13,7 @@
"TABLE_HEADER": {
"NAME": "الاسم",
"DESCRIPTION": "الوصف",
"COLOR": "اللون",
"ACTION": "الإجراءات"
"COLOR": "اللون"
}
},
"FORM": {
@@ -3,12 +3,9 @@
"HEADER": "ماكروس",
"DESCRIPTION": "الماكرو هو مجموعة من الإجراءات المحفوظة التي تساعد وكلاء خدمة العملاء على إكمال المهام بسهولة. يمكن للوكلاء تحديد مجموعة من الإجراءات مثل وضع علامة على محادثة مع تسمية، وإرسال نص بريد إلكتروني، وتحديث سمة مخصصة، إلخ. ويمكنهم تنفيذ هذه الإجراءات بنقرة واحدة.",
"LEARN_MORE": "تعلم المزيد حول الماكرو",
"COUNT": "{n} macro | {n} macros",
"HEADER_BTN_TXT": "إضافة ماكرو جديد",
"HEADER_BTN_TXT_SAVE": "حفظ الماكرو",
"LOADING": "جاري جلب الماكروس",
"SEARCH_PLACEHOLDER": "Search macros...",
"NO_RESULTS": "No macros found matching your search",
"ERROR": "حدث خطأ ما. الرجاء المحاولة مرة أخرى",
"ORDER_INFO": "سيتم تشغيل الماكرو بالترتيب الذي تضيفه إجراءاتك. يمكنك إعادة ترتيبهم بسحبهم بواسطة المعالج بجانب كل عقدة.",
"ADD": {
@@ -32,8 +29,7 @@
"NAME": "الاسم",
"CREATED BY": "تم إنشاؤها بواسطة",
"LAST_UPDATED_BY": "آخر تحديث بواسطة",
"VISIBILITY": "الظهور",
"ACTIONS": "الإجراءات"
"VISIBILITY": "الظهور"
},
"404": "لم يتم العثور على الماكروس"
},
@@ -1,7 +1,7 @@
{
"MFA_SETTINGS": {
"TITLE": "Two-Factor Authentication",
"SUBTITLE": "Protect your account from unauthorized access with TOTP-based authentication. This adds an extra layer of security to your account.",
"SUBTITLE": "Secure your account with TOTP-based authentication",
"DESCRIPTION": "Add an extra layer of security to your account using a time-based one-time password (TOTP)",
"STATUS_TITLE": "Authentication Status",
"STATUS_DESCRIPTION": "Manage your two-factor authentication settings and backup recovery codes",
@@ -3,7 +3,7 @@
"HEADER": "المحادثات",
"LOADING_CHART": "تحميل بيانات الرسم البياني...",
"NO_ENOUGH_DATA": "لم يتم جمع بيانات بقدر كافي لإنشاء التقرير، الرجاء المحاولة مرة أخرى لاحقاً.",
"DOWNLOAD_CONVERSATION_REPORTS": نزيل تقارير المحادثات",
"DOWNLOAD_AGENT_REPORTS": حميل تقارير وكيل",
"DATA_FETCHING_FAILED": "Failed to fetch data, please try again later.",
"SUMMARY_FETCHING_FAILED": "Failed to fetch summary, please try again later.",
"METRICS": {
@@ -128,16 +128,11 @@
},
"AGENT_REPORTS": {
"HEADER": "نظرة عامة للوكلاء",
"DESCRIPTION": "Easily track agent performance with key metrics such as conversations, response times, resolution times, and resolved cases. Click an agent's name to learn more.",
"DESCRIPTION": "Easily track agent performance with key metrics such as conversations, response times, resolution times, and resolved cases. Click an agents name to learn more.",
"LOADING_CHART": "جاري جلب بيانات الرسم البياني...",
"NO_ENOUGH_DATA": "لم يتم جمع بيانات بقدر كافي لإنشاء التقرير، الرجاء المحاولة مرة أخرى لاحقاً.",
"DOWNLOAD_AGENT_REPORTS": "تنزيل تقارير الوكيل",
"FILTER_DROPDOWN_LABEL": "اختر وكيل",
"FILTERS": {
"INPUT_PLACEHOLDER": {
"AGENTS": "البحث عن وكلاء"
}
},
"METRICS": {
"CONVERSATIONS": {
"NAME": "المحادثات",
@@ -206,11 +201,6 @@
"NO_ENOUGH_DATA": "لم يتم جمع بيانات بقدر كافي لإنشاء التقرير، الرجاء المحاولة مرة أخرى لاحقاً.",
"DOWNLOAD_LABEL_REPORTS": "تحميل تقارير التسمية",
"FILTER_DROPDOWN_LABEL": "حدد التسمية",
"FILTERS": {
"INPUT_PLACEHOLDER": {
"LABELS": "ابحث عن تصنيفات"
}
},
"METRICS": {
"CONVERSATIONS": {
"NAME": "المحادثات",
@@ -281,11 +271,6 @@
"FILTER_DROPDOWN_LABEL": "اختر صندوق الوارد",
"ALL_INBOXES": "All Inboxes",
"SEARCH_INBOX": "Search Inbox",
"FILTERS": {
"INPUT_PLACEHOLDER": {
"INBOXES": "Search inboxes"
}
},
"METRICS": {
"CONVERSATIONS": {
"NAME": "المحادثات",
@@ -349,19 +334,11 @@
},
"TEAM_REPORTS": {
"HEADER": "نظرة عامة للفريق",
"DESCRIPTION": "Get a snapshot of your team's performance with essential metrics, including conversations, response times, resolution times, and resolved cases. Click a team name for more details.",
"DESCRIPTION": "Get a snapshot of your teams performance with essential metrics, including conversations, response times, resolution times, and resolved cases. Click a team name for more details.",
"LOADING_CHART": "تحميل بيانات الرسم البياني...",
"NO_ENOUGH_DATA": "لم يتم جمع بيانات بقدر كافي لإنشاء التقرير، الرجاء المحاولة مرة أخرى لاحقاً.",
"DOWNLOAD_TEAM_REPORTS": "تحميل تقارير الفريق",
"FILTER_DROPDOWN_LABEL": "اختيار فريق",
"FILTERS": {
"ADD_FILTER": "إضافة تصفية",
"CLEAR_ALL": "مسح الكل",
"NO_FILTER": "لا توجد عوامل تصفية متوفرة",
"INPUT_PLACEHOLDER": {
"TEAMS": "البحث عن فريق"
}
},
"METRICS": {
"CONVERSATIONS": {
"NAME": "المحادثات",
@@ -425,48 +402,22 @@
},
"CSAT_REPORTS": {
"HEADER": "تقارير CSAT",
"NO_RECORDS": "No responses yet",
"NO_RECORDS_DESCRIPTION": "CSAT survey responses will appear here once customers start providing feedback.",
"NO_RECORDS": "لا توجد ردود متوفرة على الدراسة الاستقصائية CSAT.",
"DOWNLOAD": "تحميل تقرير رضاء خدمة العملاء",
"DOWNLOAD_FAILED": "Failed to download CSAT Reports",
"FILTERS": {
"ADD_FILTER": "إضافة تصفية",
"CLEAR_ALL": "مسح الكل",
"NO_FILTER": "لا توجد عوامل تصفية متوفرة",
"INPUT_PLACEHOLDER": {
"AGENTS": "البحث عن وكلاء",
"INBOXES": "Search inboxes",
"TEAMS": "البحث عن فريق",
"RATINGS": "البحث في التقييمات"
},
"AGENTS": {
"LABEL": "وكيل الدعم"
},
"INBOXES": {
"LABEL": "صندوق الوارد"
},
"TEAMS": {
"LABEL": "الفريق"
},
"RATINGS": {
"LABEL": "التقييم"
"PLACEHOLDER": "اختر الوكلاء"
}
},
"TABLE": {
"HEADER": {
"CONTACT_NAME": "جهات الاتصال",
"AGENT_NAME": "وكيل الدعم",
"AGENT_NAME": "الوكيل المكلف",
"RATING": "التقييم",
"FEEDBACK_TEXT": "تعليق الملاحظات",
"CONVERSATION": "المحادثات",
"CUSTOMER": "عميل",
"RESPONSE": "الردود",
"HANDLED_BY": "تمت معالجتها بواسطة"
},
"UNKNOWN_CUSTOMER": "عميل غير معروف"
"FEEDBACK_TEXT": "تعليق الملاحظات"
}
},
"NO_AGENT": "لم يتم تعيين وكيل",
"NO_FEEDBACK": "لا توجد ملاحظات مقدمة",
"METRIC": {
"TOTAL_RESPONSES": {
"LABEL": "إجمالي الردود",
@@ -479,25 +430,6 @@
"RESPONSE_RATE": {
"LABEL": "معدل الاستجابة",
"TOOLTIP": "العدد الإجمالي للردود / العدد الإجمالي لرسائل الاستقصاء التي أرسلتها CSAT * 100"
},
"RATING_DISTRIBUTION": "توزيع التقييم"
},
"REVIEW_NOTES": {
"TITLE": "ملاحظات المراجعة",
"PLACEHOLDER": "إضافة ملاحظات مراجعة حول هذا التقييم...",
"SAVE": "حفظ",
"CANCEL": "إلغاء",
"SAVING": "جاري الحفظ...",
"SAVED": "تم حفظ الملاحظات بنجاح",
"SAVE_ERROR": "فشل في حفظ الملاحظات",
"UPDATED_BY": "تم التحديث بواسطة {name} {time}",
"UPDATED_BY_LABEL": "تم التحديث بواسطة",
"PAYWALL": {
"TITLE": "قم بالترقية لإضافة ملاحظات المراجعة",
"AVAILABLE_ON": "ميزة مراجعة الملاحظات متاحة فقط في الخطط Business و Enterprise.",
"UPGRADE_PROMPT": "Add internal context to every CSAT response with review notes. Capture what really happened, spot patterns faster, and make better decisions from your feedback.",
"UPGRADE_NOW": "الترقية الآن",
"CANCEL_ANYTIME": "You can change or cancel your plan anytime"
}
}
},
@@ -1,7 +1,7 @@
{
"SEARCH": {
"TABS": {
"ALL": "كل النتائج",
"ALL": "All results",
"CONTACTS": "جهات الاتصال",
"CONVERSATIONS": "المحادثات",
"MESSAGES": "الرسائل",
@@ -21,48 +21,48 @@
"EMPTY_STATE_FULL": "لم يتم العثور على نتائج للطلب '{query}'",
"PLACEHOLDER_KEYBINDING": "/للتركيز",
"INPUT_PLACEHOLDER": "أكتب 3 أحرف أو أكثر للبحث",
"RECENT_SEARCHES": "عمليات البحث الأخيرة",
"RECENT_SEARCHES": "Recent searches",
"CLEAR_ALL": "Clear all",
"MOST_RECENT": "الأحدث",
"MOST_RECENT": "Most recent",
"EMPTY_STATE_DEFAULT": "البحث عن طريق معرف المحادثة أو البريد الإلكتروني أو رقم الهاتف أو الرسائل للحصول على نتائج بحث أفضل. ",
"BOT_LABEL": "رد آلي",
"READ_MORE": "اقرأ المزيد",
"READ_LESS": "قراءة أقل",
"READ_LESS": "Read less",
"WROTE": "كتب:",
"FROM": "من",
"EMAIL": "البريد الإلكتروني",
"EMAIL_SUBJECT": "الموضوع",
"PRIVATE": "ملاحظة خاصة",
"TRANSCRIPT": "النص",
"PRIVATE": "Private note",
"TRANSCRIPT": "Transcript",
"CREATED_AT": "created {time}",
"UPDATED_AT": "تم التحديث {time}",
"UPDATED_AT": "updated {time}",
"SORT_BY": {
"RELEVANCE": "ذات صلة"
"RELEVANCE": "Relevance"
},
"DATE_RANGE": {
"LAST_7_DAYS": "آخر 7 أيام",
"LAST_30_DAYS": "آخر 30 يوماً",
"LAST_60_DAYS": "آخر 60 يوماً",
"LAST_90_DAYS": "آخر 90 يوماً",
"CUSTOM_RANGE": "نطاق مخصص:",
"CREATED_BETWEEN": "تم الإنشاء بين",
"CUSTOM_RANGE": "Custom range:",
"CREATED_BETWEEN": "Created between",
"AND": "و",
"APPLY": "تطبيق",
"BEFORE_DATE": "قبل {date}",
"AFTER_DATE": "بعد {date}",
"TIME_RANGE": "التصفية حسب الوقت",
"CLEAR_FILTER": "مسح عامل التصفية"
"BEFORE_DATE": "Before {date}",
"AFTER_DATE": "After {date}",
"TIME_RANGE": "Filter by time",
"CLEAR_FILTER": "Clear filter"
},
"FILTERS": {
"FILTER_MESSAGE": "تصفية الرسائل بواسطة:",
"FILTER_MESSAGE": "Filter messages by:",
"FROM": "المرسل",
"IN": "صندوق الوارد",
"AGENTS": "الوكلاء",
"CONTACTS": "جهات الاتصال",
"INBOXES": "قنوات التواصل",
"NO_AGENTS": "لم يتم العثور على وكلاء",
"NO_CONTACTS": "ابدأ بالبحث لمشاهدة النتائج",
"NO_INBOXES": "لم يتم العثور على صناديق الوارد"
"NO_CONTACTS": "Start by searching to see results",
"NO_INBOXES": "No inboxes found"
}
}
}
@@ -273,8 +273,7 @@
"FILE_BUBBLE": {
"DOWNLOAD": "تنزيل",
"UPLOADING": "جاري الرفع...",
"INSTAGRAM_STORY_UNAVAILABLE": "هذه القصة لم تعد متاحة.",
"INSTAGRAM_STORY_REPLY": "رد على قصتك:"
"INSTAGRAM_STORY_UNAVAILABLE": "هذه القصة لم تعد متاحة."
},
"LOCATION_BUBBLE": {
"SEE_ON_MAP": "مشاهدة على الخريطة"
@@ -308,8 +307,8 @@
"SETTINGS": "الإعدادات",
"CONTACTS": "جهات الاتصال",
"ACTIVE": "مفعل",
"COMPANIES": "الشركات",
"ALL_COMPANIES": "كل الحملات",
"COMPANIES": "Companies",
"ALL_COMPANIES": "All Companies",
"CAPTAIN": "قائد",
"CAPTAIN_ASSISTANTS": "Assistants",
"CAPTAIN_DOCUMENTS": "Documents",
@@ -379,57 +378,7 @@
"INFO_SHORT": "Automatically mark offline when you aren't using the app."
},
"DOCS": "قراءة المستندات",
"SECURITY": "Security",
"CAPTAIN_AI": "قائد",
"CONVERSATION_WORKFLOW": "Conversation Workflow"
},
"CAPTAIN_SETTINGS": {
"TITLE": "Captain Settings",
"DESCRIPTION": "Configure your AI models and features for Captain. Captain follows a credit based billing, you will be charged credits for every action Captain takes based on the model selected.",
"LOADING": "Loading Captain configuration...",
"LINK_TEXT": "Learn more about Captain Credits",
"NOT_ENABLED": "Captain is not enabled for your account. Please upgrade your plan to access Captain features.",
"MODEL_CONFIG": {
"TITLE": "Model Configuration",
"DESCRIPTION": "Select AI models for different features.",
"SELECT_MODEL": "Select model",
"CREDITS_PER_MESSAGE": "{credits} credit/message",
"COMING_SOON": "Coming soon",
"EDITOR": {
"TITLE": "Editor Features",
"DESCRIPTION": "Powers smart compose, grammar corrections, tone adjustments, and content enhancement in your message editor."
},
"ASSISTANT": {
"TITLE": "Assistant",
"DESCRIPTION": "Handles automated responses, conversation summaries, and intelligent reply suggestions for customer interactions."
},
"COPILOT": {
"TITLE": "Co-pilot",
"DESCRIPTION": "Provides real-time contextual suggestions, knowledge base recommendations, and proactive insights during conversations."
}
},
"FEATURES": {
"TITLE": "الخصائص",
"DESCRIPTION": "Enable or disable AI-powered features.",
"AUDIO_TRANSCRIPTION": {
"TITLE": "Audio Transcription",
"DESCRIPTION": "Automatically convert voice messages and call recordings into searchable text transcripts."
},
"HELP_CENTER_SEARCH": {
"TITLE": "Help Center Search Indexing",
"DESCRIPTION": "Use AI for context aware search inside your help center articles."
},
"LABEL_SUGGESTION": {
"TITLE": "Label Suggestion",
"DESCRIPTION": "Automatically suggest relevant labels and tags for conversations based on content analysis and context.",
"MODEL_TITLE": "Label Suggestion Model",
"MODEL_DESCRIPTION": "Select the AI model to use for analyzing conversations and suggesting appropriate labels"
}
},
"API": {
"SUCCESS": "Captain settings updated successfully.",
"ERROR": "Failed to update Captain settings. Please try again."
}
"SECURITY": "Security"
},
"BILLING_SETTINGS": {
"TITLE": "الفواتير",
@@ -557,58 +506,6 @@
}
}
},
"CONVERSATION_WORKFLOW": {
"INDEX": {
"HEADER": {
"TITLE": "Conversation Workflows",
"DESCRIPTION": "Configure rules and required fields for conversation resolution."
}
},
"REQUIRED_ATTRIBUTES": {
"TITLE": "Attributes required on resolution",
"DESCRIPTION": "When resolving a conversation, agents will be prompted to fill these attributes if they haven't yet.",
"NO_ATTRIBUTES": "No attributes added yet",
"ADD": {
"TITLE": "Add Attributes",
"SEARCH_PLACEHOLDER": "البحث عن صفات"
},
"SAVE": {
"SUCCESS": "Required attributes updated",
"ERROR": "Could not update required attributes, please try again"
},
"MODAL": {
"TITLE": "حل المحادثة",
"DESCRIPTION": "Please fill in the following custom attributes before resolving this conversation",
"ACTIONS": {
"RESOLVE": "حل المحادثة",
"CANCEL": "إلغاء"
},
"PLACEHOLDERS": {
"TEXT": "Write a note...",
"NUMBER": "Enter a number",
"LINK": "Add a link",
"DATE": "Pick a date",
"LIST": "Select an option"
},
"CHECKBOX": {
"YES": "نعم",
"NO": "لا"
}
},
"PAYWALL": {
"TITLE": "Upgrade to use required attributes",
"AVAILABLE_ON": "The required conversation attributes feature is available on the Business and Enterprise plans.",
"UPGRADE_PROMPT": "Upgrade your plan to prompt agents to fill required attributes before conversation resolution.",
"UPGRADE_NOW": "Upgrade now",
"CANCEL_ANYTIME": "You can change or cancel your plan anytime"
},
"ENTERPRISE_PAYWALL": {
"AVAILABLE_ON": "The required conversation attributes feature is available on the paid plans.",
"UPGRADE_PROMPT": "Upgrade to a paid plan to enforce required attributes before conversation resolution.",
"ASK_ADMIN": "Please reach out to your administrator for the upgrade."
}
}
},
"CREATE_ACCOUNT": {
"NO_ACCOUNT_WARNING": "أوه! لم نتمكن من العثور على الحساب. الرجاء إنشاء حساب جديد للمتابعة.",
"NEW_ACCOUNT": "حساب جديد",
@@ -694,8 +591,7 @@
"CREATE_BUTTON": "Create policy",
"API": {
"SUCCESS_MESSAGE": "Assignment policy created successfully",
"ERROR_MESSAGE": "Failed to create assignment policy",
"INBOX_LINKED": "Inbox has been linked to the policy"
"ERROR_MESSAGE": "Failed to create assignment policy"
}
},
"EDIT": {
@@ -709,12 +605,6 @@
"CONFIRM_BUTTON_LABEL": "Continue",
"CANCEL_BUTTON_LABEL": "إلغاء"
},
"INBOX_LINK_PROMPT": {
"TITLE": "Link inbox to policy",
"DESCRIPTION": "Would you like to link this inbox to the assignment policy?",
"LINK_BUTTON": "Link inbox",
"CANCEL_BUTTON": "Skip"
},
"API": {
"SUCCESS_MESSAGE": "Assignment policy updated successfully",
"ERROR_MESSAGE": "Failed to update assignment policy"
@@ -753,9 +643,7 @@
},
"BALANCED": {
"LABEL": "Balanced",
"DESCRIPTION": "Assign conversations based on available capacity.",
"PREMIUM_MESSAGE": "Upgrade to access balanced assignment and agent capacity management.",
"PREMIUM_BADGE": "Premium"
"DESCRIPTION": "Assign conversations based on available capacity."
}
},
"ASSIGNMENT_PRIORITY": {
@@ -841,20 +729,6 @@
"SUCCESS_MESSAGE": "Agent removed from policy successfully",
"ERROR_MESSAGE": "Failed to remove agent from policy"
}
},
"INBOX_LIMIT_API": {
"ADD": {
"SUCCESS_MESSAGE": "Inbox limit added successfully",
"ERROR_MESSAGE": "Failed to add inbox limit"
},
"UPDATE": {
"SUCCESS_MESSAGE": "Inbox limit updated successfully",
"ERROR_MESSAGE": "Failed to update inbox limit"
},
"DELETE": {
"SUCCESS_MESSAGE": "Inbox limit deleted successfully",
"ERROR_MESSAGE": "Failed to delete inbox limit"
}
}
},
"FORM": {
@@ -1,7 +1,6 @@
{
"REGISTER": {
"TRY_WOOT": "تسجيل حساب",
"GET_STARTED": "Get started with Chatwoot",
"TITLE": "تسجيل",
"TESTIMONIAL_HEADER": "إن كل ما يلزم هو خطوة واحدة للمضي قدما",
"TESTIMONIAL_CONTENT": "You're one step away from engaging your customers, retaining them and finding new ones.",
@@ -5,12 +5,7 @@
"ADD_ACTION_LONG": "Create a new SLA Policy",
"DESCRIPTION": "Service Level Agreements (SLAs) are contracts that define clear expectations between your team and customers. They establish standards for response and resolution times, creating a framework for accountability and ensures a consistent, high-quality experience.",
"LEARN_MORE": "Learn more about SLA",
"COUNT": "{n} SLA | {n} SLAs",
"LOADING": "Fetching SLAs",
"SEARCH_PLACEHOLDER": "Search SLA...",
"SEARCH": {
"NO_RESULTS": "No SLA found matching your search"
},
"PAYWALL": {
"TITLE": "Upgrade to create SLAs",
"AVAILABLE_ON": "The SLA feature is only available in the Business and Enterprise plans.",
@@ -25,18 +20,14 @@
},
"LIST": {
"404": "There are no SLAs available in this account.",
"TABLE_HEADER": {
"SLA": "SLA",
"BUSINESS_HOURS": "Business hours"
},
"EMPTY": {
"TITLE_1": "Enterprise P0",
"DESC_1": "Issues raised by enterprise customers, that require immediate attention.",
"TITLE_2": "Enterprise P1",
"DESC_2": "Issues raised by enterprise customers, that needs to be acknowledged quickly."
},
"BUSINESS_HOURS_ON": "Turned on",
"BUSINESS_HOURS_OFF": "Turned off",
"BUSINESS_HOURS_ON": "Business hours on",
"BUSINESS_HOURS_OFF": "Business hours off",
"RESPONSE_TYPES": {
"FRT": "First response time threshold",
"NRT": "Next response time threshold",
@@ -1,72 +0,0 @@
{
"SNOOZE_PARSER": {
"UNITS": {
"MINUTE": "minute",
"MINUTES": "minutes",
"HOUR": "hour",
"HOURS": "ساعات",
"DAY": "اليوم",
"DAYS": "days",
"WEEK": "اليوم",
"WEEKS": "weeks",
"MONTH": "الأسبوع",
"MONTHS": "months",
"YEAR": "الشهر",
"YEARS": "years"
},
"HALF": "half",
"NEXT": "التالي",
"THIS": "this",
"AT": "at",
"IN": "in",
"FROM_NOW": "from now",
"NEXT_YEAR": "next year",
"MERIDIEM": {
"AM": "am",
"PM": "pm"
},
"RELATIVE": {
"TOMORROW": "غداً",
"DAY_AFTER_TOMORROW": "day after tomorrow",
"NEXT_WEEK": "الأسبوع القادم",
"NEXT_MONTH": "next month",
"THIS_WEEKEND": "this weekend",
"NEXT_WEEKEND": "next weekend"
},
"TIME_OF_DAY": {
"MORNING": "morning",
"AFTERNOON": "afternoon",
"EVENING": "evening",
"NIGHT": "night",
"NOON": "noon",
"MIDNIGHT": "midnight"
},
"WORD_NUMBERS": {
"ONE": "one",
"TWO": "two",
"THREE": "three",
"FOUR": "four",
"FIVE": "five",
"SIX": "six",
"SEVEN": "seven",
"EIGHT": "eight",
"NINE": "nine",
"TEN": "ten",
"TWELVE": "twelve",
"FIFTEEN": "fifteen",
"TWENTY": "twenty",
"THIRTY": "thirty"
},
"ORDINALS": {
"FIRST": "first",
"SECOND": "second",
"THIRD": "third",
"FOURTH": "fourth",
"FIFTH": "fifth"
},
"OF": "of",
"AFTER": "after",
"WEEK": "اليوم",
"DAY": "اليوم"
}
}
@@ -5,9 +5,6 @@
"LOADING": "Fetching teams",
"DESCRIPTION": "الفرق تتيح لك تنظيم الوكلاء في مجموعات بناءً على مسؤولياتهم. يمكن للوكيل أن يكون عضوًا في أكثر من فريق. لتحقيق التعاون في العمل, يمكنك إسناد المحادثات لفرق محددة.",
"LEARN_MORE": "لمعرفة المزيد حول الفرق",
"COUNT": "{n} team | {n} teams",
"SEARCH_PLACEHOLDER": "البحث عن فريق...",
"NO_RESULTS": "No teams found matching your search",
"LIST": {
"404": "لا يوجد موظفي دعم مرتبطين بهذا الحساب.",
"EDIT_TEAM": "تعديل الفريق",
@@ -67,7 +64,7 @@
"ERROR_MESSAGE": "تعذر حفظ تفاصيل الفريق. حاول مرة أخرى."
},
"AGENTS": {
"AGENT": "وكيل الدعم",
"AGENT": "وكيل",
"EMAIL": "البريد الإلكتروني",
"BUTTON_TEXT": "إضافة وكلاء",
"ADD_AGENTS": "إضافة وكلاء إلى فريقك...",
@@ -4,9 +4,6 @@
"LOADING_EDITOR": "Loading editor...",
"DESCRIPTION": "Agent Bots are like the most fabulous members of your team. They can handle the small stuff, so you can focus on the stuff that matters. Give them a try. You can manage your bots from this page or create new ones using the 'Add Bot' button.",
"LEARN_MORE": "Learn about agent bots",
"COUNT": "{n} bot | {n} bots",
"SEARCH_PLACEHOLDER": "Search bots...",
"NO_RESULTS": "No bots found matching your search",
"GLOBAL_BOT": "System bot",
"GLOBAL_BOT_BADGE": "System",
"AVATAR": {
@@ -37,8 +34,7 @@
"LOADING": "Fetching bots...",
"TABLE_HEADER": {
"DETAILS": "Bot Details",
"URL": "Webhook URL",
"ACTIONS": "Actions"
"URL": "Webhook URL"
}
},
"DELETE": {
@@ -9,7 +9,6 @@
"ADMINISTRATOR": "Administrator",
"AGENT": "Agent"
},
"COUNT": "{n} agent | {n} agents",
"LIST": {
"404": "There are no agents associated to this account",
"TITLE": "Manage agents in your team",
@@ -97,8 +96,6 @@
"ERROR_MESSAGE": "Could not connect to Woot Server, Please try again later"
}
},
"SEARCH_PLACEHOLDER": "Search agents...",
"NO_RESULTS": "No agents found matching your search",
"SEARCH": {
"NO_RESULTS": "No results found."
},
@@ -5,9 +5,6 @@
"LOADING": "Fetching custom attributes",
"DESCRIPTION": "A custom attribute tracks additional details about your contacts or conversations—such as the subscription plan or the date of their first purchase. You can add different types of custom attributes, such as text, lists, or numbers, to capture the specific information you need.",
"LEARN_MORE": "Learn more about custom attributes",
"COUNT": "{n} attribute | {n} attributes",
"SEARCH_PLACEHOLDER": "Search attributes...",
"NO_RESULTS": "No attributes found matching your search",
"ATTRIBUTE_MODELS": {
"CONVERSATION": "Conversation",
"CONTACT": "Contact"
@@ -66,10 +63,6 @@
},
"ENABLE_REGEX": {
"LABEL": "Enable regex validation"
},
"BADGES": {
"PRE_CHAT": "Pre-chat",
"RESOLUTION": "Resolution"
}
},
"API": {

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