feat: Add Google Translate API Integration (#6454)
This commit is contained in:
2
Gemfile
2
Gemfile
@@ -41,6 +41,8 @@ gem 'down', '~> 5.0'
|
||||
gem 'gmail_xoauth'
|
||||
# Prevent CSV injection
|
||||
gem 'csv-safe'
|
||||
# Support message translation
|
||||
gem 'google-cloud-translate'
|
||||
|
||||
##-- for active storage --##
|
||||
gem 'aws-sdk-s3', require: false
|
||||
|
||||
14
Gemfile.lock
14
Gemfile.lock
@@ -289,6 +289,19 @@ GEM
|
||||
google-cloud-core (~> 1.6)
|
||||
googleauth (>= 0.16.2, < 2.a)
|
||||
mini_mime (~> 1.0)
|
||||
google-cloud-translate (3.3.0)
|
||||
google-cloud-core (~> 1.6)
|
||||
google-cloud-translate-v2 (>= 0.0, < 2.a)
|
||||
google-cloud-translate-v3 (>= 0.0, < 2.a)
|
||||
google-cloud-translate-v2 (0.4.0)
|
||||
faraday (>= 0.17.3, < 2.a)
|
||||
google-cloud-core (~> 1.6)
|
||||
googleapis-common-protos (>= 1.3.10, < 2.a)
|
||||
googleapis-common-protos-types (>= 1.0.5, < 2.a)
|
||||
googleauth (>= 0.16.2, < 2.a)
|
||||
google-cloud-translate-v3 (0.5.0)
|
||||
gapic-common (>= 0.10, < 2.a)
|
||||
google-cloud-errors (~> 1.0)
|
||||
google-protobuf (3.21.7)
|
||||
google-protobuf (3.21.7-x86_64-darwin)
|
||||
google-protobuf (3.21.7-x86_64-linux)
|
||||
@@ -774,6 +787,7 @@ DEPENDENCIES
|
||||
gmail_xoauth
|
||||
google-cloud-dialogflow
|
||||
google-cloud-storage
|
||||
google-cloud-translate
|
||||
groupdate
|
||||
haikunator
|
||||
hairtrigger
|
||||
|
||||
@@ -18,6 +18,24 @@ class Api::V1::Accounts::Conversations::MessagesController < Api::V1::Accounts::
|
||||
end
|
||||
end
|
||||
|
||||
def translate
|
||||
return head :ok if already_translated_content_available?
|
||||
|
||||
translated_content = Integrations::GoogleTranslate::ProcessorService.new(
|
||||
message: message,
|
||||
target_language: permitted_params[:target_language]
|
||||
).perform
|
||||
|
||||
if translated_content.present?
|
||||
translations = {}
|
||||
translations[permitted_params[:target_language]] = translated_content
|
||||
translations = message.translations.merge!(translations) if message.translations.present?
|
||||
message.update!(translations: translations)
|
||||
end
|
||||
|
||||
render json: { content: translated_content }
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def message
|
||||
@@ -29,6 +47,10 @@ class Api::V1::Accounts::Conversations::MessagesController < Api::V1::Accounts::
|
||||
end
|
||||
|
||||
def permitted_params
|
||||
params.permit(:id)
|
||||
params.permit(:id, :target_language)
|
||||
end
|
||||
|
||||
def already_translated_content_available?
|
||||
message.translations.present? && message.translations[permitted_params[:target_language]].present?
|
||||
end
|
||||
end
|
||||
|
||||
@@ -80,6 +80,15 @@ class MessageApi extends ApiClient {
|
||||
params: { before },
|
||||
});
|
||||
}
|
||||
|
||||
translateMessage(conversationId, messageId, targetLanguage) {
|
||||
return axios.post(
|
||||
`${this.url}/${conversationId}/messages/${messageId}/translate`,
|
||||
{
|
||||
target_language: targetLanguage,
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default new MessageApi();
|
||||
|
||||
@@ -73,6 +73,39 @@
|
||||
:created-at="createdAt"
|
||||
/>
|
||||
</div>
|
||||
<woot-modal
|
||||
v-if="showTranslateModal"
|
||||
modal-type="right-aligned"
|
||||
show
|
||||
:on-close="onCloseTranslateModal"
|
||||
>
|
||||
<div class="column content">
|
||||
<p>
|
||||
<b>{{ $t('TRANSLATE_MODAL.ORIGINAL_CONTENT') }}</b>
|
||||
</p>
|
||||
<p v-dompurify-html="data.content" />
|
||||
<br />
|
||||
<hr />
|
||||
<div v-if="translationsAvailable">
|
||||
<p>
|
||||
<b>{{ $t('TRANSLATE_MODAL.TRANSLATED_CONTENT') }}</b>
|
||||
</p>
|
||||
<div
|
||||
v-for="(translation, language) in translations"
|
||||
:key="language"
|
||||
>
|
||||
<p>
|
||||
<strong>{{ language }}:</strong>
|
||||
</p>
|
||||
<p v-dompurify-html="translation" />
|
||||
<br />
|
||||
</div>
|
||||
</div>
|
||||
<p v-else>
|
||||
{{ $t('TRANSLATE_MODAL.NO_TRANSLATIONS_AVAILABLE') }}
|
||||
</p>
|
||||
</div>
|
||||
</woot-modal>
|
||||
<spinner v-if="isPending" size="tiny" />
|
||||
<div
|
||||
v-if="showAvatar"
|
||||
@@ -116,6 +149,7 @@
|
||||
:message-content="data.content"
|
||||
@toggle="handleContextMenuClick"
|
||||
@delete="handleDelete"
|
||||
@translate="handleTranslate"
|
||||
/>
|
||||
</div>
|
||||
</li>
|
||||
@@ -138,6 +172,7 @@ import alertMixin from 'shared/mixins/alertMixin';
|
||||
import contentTypeMixin from 'shared/mixins/contentTypeMixin';
|
||||
import { MESSAGE_TYPE, MESSAGE_STATUS } from 'shared/constants/messages';
|
||||
import { generateBotMessageContent } from './helpers/botMessageContentHelper';
|
||||
import { mapGetters } from 'vuex';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@@ -181,9 +216,14 @@ export default {
|
||||
return {
|
||||
showContextMenu: false,
|
||||
hasImageError: false,
|
||||
showTranslateModal: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
getAccount: 'accounts/getAccount',
|
||||
currentAccountId: 'getCurrentAccountId',
|
||||
}),
|
||||
shouldRenderMessage() {
|
||||
return (
|
||||
this.hasAttachments ||
|
||||
@@ -199,6 +239,9 @@ export default {
|
||||
} = this.contentAttributes.email || {};
|
||||
return fullHTMLContent || fullTextContent || '';
|
||||
},
|
||||
translations() {
|
||||
return this.contentAttributes.translations || {};
|
||||
},
|
||||
displayQuotedButton() {
|
||||
if (!this.isIncoming) {
|
||||
return false;
|
||||
@@ -210,6 +253,9 @@ export default {
|
||||
|
||||
return false;
|
||||
},
|
||||
translationsAvailable() {
|
||||
return !!Object.keys(this.translations).length;
|
||||
},
|
||||
message() {
|
||||
if (this.contentType === 'input_csat') {
|
||||
return this.$t('CONVERSATION.CSAT_REPLY_MESSAGE');
|
||||
@@ -432,6 +478,19 @@ export default {
|
||||
onImageLoadError() {
|
||||
this.hasImageError = true;
|
||||
},
|
||||
handleTranslate() {
|
||||
const { locale } = this.getAccount(this.currentAccountId);
|
||||
const { conversation_id: conversationId, id: messageId } = this.data;
|
||||
this.$store.dispatch('translateMessage', {
|
||||
conversationId,
|
||||
messageId,
|
||||
targetLanguage: locale || 'en',
|
||||
});
|
||||
this.showTranslateModal = true;
|
||||
},
|
||||
onCloseTranslateModal() {
|
||||
this.showTranslateModal = false;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -165,7 +165,8 @@
|
||||
"CONTEXT_MENU": {
|
||||
"COPY": "Copy",
|
||||
"DELETE": "Delete",
|
||||
"CREATE_A_CANNED_RESPONSE": "Add to canned responses"
|
||||
"CREATE_A_CANNED_RESPONSE": "Add to canned responses",
|
||||
"TRANSLATE": "Translate"
|
||||
}
|
||||
},
|
||||
"EMAIL_TRANSCRIPT": {
|
||||
@@ -253,5 +254,12 @@
|
||||
"BCC": "Bcc",
|
||||
"CC": "Cc",
|
||||
"SUBJECT": "Subject"
|
||||
},
|
||||
"TRANSLATE_MODAL": {
|
||||
"TITLE": "View translated content",
|
||||
"DESC": "You can view the translated content in each langauge.",
|
||||
"ORIGINAL_CONTENT": "Original Content",
|
||||
"TRANSLATED_CONTENT": "Translated Content",
|
||||
"NO_TRANSLATIONS_AVAILABLE": "No translations are available for this content"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,6 +57,17 @@
|
||||
{{ $t('CONVERSATION.CONTEXT_MENU.CREATE_A_CANNED_RESPONSE') }}
|
||||
</woot-button>
|
||||
</woot-dropdown-item>
|
||||
<woot-dropdown-item>
|
||||
<woot-button
|
||||
variant="clear"
|
||||
size="small"
|
||||
icon="translate"
|
||||
color-scheme="secondary"
|
||||
@click="handleTranslate"
|
||||
>
|
||||
{{ $t('CONVERSATION.CONTEXT_MENU.TRANSLATE') }}
|
||||
</woot-button>
|
||||
</woot-dropdown-item>
|
||||
</woot-dropdown-menu>
|
||||
</div>
|
||||
</div>
|
||||
@@ -133,6 +144,11 @@ export default {
|
||||
this.$track(ACCOUNT_EVENTS.ADDED_TO_CANNED_RESPONSE);
|
||||
this.isCannedResponseModalOpen = true;
|
||||
},
|
||||
|
||||
handleTranslate() {
|
||||
this.$emit('translate');
|
||||
this.handleContextMenuClick();
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
import messageReadActions from './actions/messageReadActions';
|
||||
import AnalyticsHelper from '../../../helper/AnalyticsHelper';
|
||||
import { CONVERSATION_EVENTS } from '../../../helper/AnalyticsHelper/events';
|
||||
import messageTranslateActions from './actions/messageTranslateActions';
|
||||
// actions
|
||||
const actions = {
|
||||
getConversation: async ({ commit }, conversationId) => {
|
||||
@@ -341,6 +342,7 @@ const actions = {
|
||||
commit(types.CLEAR_CONVERSATION_FILTERS);
|
||||
},
|
||||
...messageReadActions,
|
||||
...messageTranslateActions,
|
||||
};
|
||||
|
||||
export default actions;
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import MessageApi from '../../../../api/inbox/message';
|
||||
|
||||
export default {
|
||||
async translateMessage(_, { conversationId, messageId, targetLanguage }) {
|
||||
try {
|
||||
await MessageApi.translateMessage(
|
||||
conversationId,
|
||||
messageId,
|
||||
targetLanguage
|
||||
);
|
||||
} catch (error) {
|
||||
// ignore error
|
||||
}
|
||||
},
|
||||
};
|
||||
@@ -17,7 +17,7 @@ const state = {
|
||||
};
|
||||
|
||||
const isAValidAppIntegration = integration => {
|
||||
return ['dialogflow', 'dyte'].includes(integration.id);
|
||||
return ['dialogflow', 'dyte', 'google_translate'].includes(integration.id);
|
||||
};
|
||||
export const getters = {
|
||||
getIntegrations($state) {
|
||||
|
||||
@@ -170,5 +170,6 @@
|
||||
"pane-close-outline": "M9.193 9.249a.75.75 0 0 1 1.059-.056l2.5 2.25a.75.75 0 0 1 0 1.114l-2.5 2.25a.75.75 0 0 1-1.004-1.115l1.048-.942H6.75a.75.75 0 1 1 0-1.5h3.546l-1.048-.942a.75.75 0 0 1-.055-1.06ZM22 17.25A2.75 2.75 0 0 1 19.25 20H4.75A2.75 2.75 0 0 1 2 17.25V6.75A2.75 2.75 0 0 1 4.75 4h14.5A2.75 2.75 0 0 1 22 6.75v10.5Zm-2.75 1.25c.69 0 1.25-.56 1.25-1.25V6.749c0-.69-.56-1.25-1.25-1.25h-3.254V18.5h3.254Zm-4.754 0V5.5H4.75c-.69 0-1.25.56-1.25 1.25v10.5c0 .69.56 1.25 1.25 1.25h9.746Z",
|
||||
"chevron-left-solid": "M15.707 4.293a1 1 0 0 1 0 1.414L9.414 12l6.293 6.293a1 1 0 0 1-1.414 1.414l-7-7a1 1 0 0 1 0-1.414l7-7a1 1 0 0 1 1.414 0Z",
|
||||
"chevron-right-solid": "M8.293 4.293a1 1 0 0 0 0 1.414L14.586 12l-6.293 6.293a1 1 0 1 0 1.414 1.414l7-7a1 1 0 0 0 0-1.414l-7-7a1 1 0 0 0-1.414 0Z",
|
||||
"comment-add-outline": "M12.022 3a6.473 6.473 0 0 0-.709 1.5H5.25A1.75 1.75 0 0 0 3.5 6.25v8.5c0 .966.784 1.75 1.75 1.75h2.249v3.75l5.015-3.75h6.236a1.75 1.75 0 0 0 1.75-1.75l.001-2.483a6.517 6.517 0 0 0 1.5-1.077L22 14.75A3.25 3.25 0 0 1 18.75 18h-5.738L8 21.75a1.25 1.25 0 0 1-1.999-1V18h-.75A3.25 3.25 0 0 1 2 14.75v-8.5A3.25 3.25 0 0 1 5.25 3h6.772ZM17.5 1a5.5 5.5 0 1 1 0 11 5.5 5.5 0 0 1 0-11Zm0 1.5-.09.008a.5.5 0 0 0-.402.402L17 3l-.001 3H14l-.09.008a.5.5 0 0 0-.402.402l-.008.09.008.09a.5.5 0 0 0 .402.402L14 7h2.999L17 10l.008.09a.5.5 0 0 0 .402.402l.09.008.09-.008a.5.5 0 0 0 .402-.402L18 10l-.001-3H21l.09-.008a.5.5 0 0 0 .402-.402l.008-.09-.008-.09a.5.5 0 0 0-.402-.402L21 6h-3.001L18 3l-.008-.09a.5.5 0 0 0-.402-.402L17.5 2.5Z"
|
||||
"comment-add-outline": "M12.022 3a6.473 6.473 0 0 0-.709 1.5H5.25A1.75 1.75 0 0 0 3.5 6.25v8.5c0 .966.784 1.75 1.75 1.75h2.249v3.75l5.015-3.75h6.236a1.75 1.75 0 0 0 1.75-1.75l.001-2.483a6.517 6.517 0 0 0 1.5-1.077L22 14.75A3.25 3.25 0 0 1 18.75 18h-5.738L8 21.75a1.25 1.25 0 0 1-1.999-1V18h-.75A3.25 3.25 0 0 1 2 14.75v-8.5A3.25 3.25 0 0 1 5.25 3h6.772ZM17.5 1a5.5 5.5 0 1 1 0 11 5.5 5.5 0 0 1 0-11Zm0 1.5-.09.008a.5.5 0 0 0-.402.402L17 3l-.001 3H14l-.09.008a.5.5 0 0 0-.402.402l-.008.09.008.09a.5.5 0 0 0 .402.402L14 7h2.999L17 10l.008.09a.5.5 0 0 0 .402.402l.09.008.09-.008a.5.5 0 0 0 .402-.402L18 10l-.001-3H21l.09-.008a.5.5 0 0 0 .402-.402l.008-.09-.008-.09a.5.5 0 0 0-.402-.402L21 6h-3.001L18 3l-.008-.09a.5.5 0 0 0-.402-.402L17.5 2.5Z",
|
||||
"translate-outline": "M16.953 5.303a1 1 0 0 0-1.906-.606c-.124.389-.236.899-.324 1.344-.565.012-1.12 0-1.652-.038a1 1 0 1 0-.142 1.995c.46.032.934.048 1.416.047a25.649 25.649 0 0 0-.24 1.698c-1.263.716-2.142 1.684-2.636 2.7-.624 1.283-.7 2.857.239 3.883.675.736 1.704.758 2.499.588.322-.068.654-.176.988-.32a1 1 0 0 0 1.746-.93 13.17 13.17 0 0 0-.041-.115 8.404 8.404 0 0 0 2.735-4.06c.286.251.507.55.658.864.284.594.334 1.271.099 1.91-.234.633-.78 1.313-1.84 1.843a1 1 0 0 0 .895 1.789c1.44-.72 2.385-1.758 2.821-2.94a4.436 4.436 0 0 0-.17-3.464 4.752 4.752 0 0 0-2.104-2.165C19.998 9.22 20 9.11 20 9a1 1 0 0 0-1.974-.23 5.984 5.984 0 0 0-1.796.138c.047-.305.102-.626.166-.964a20.142 20.142 0 0 0 2.842-.473 1 1 0 0 0-.476-1.942c-.622.152-1.286.272-1.964.358.048-.208.1-.409.155-.584Zm-3.686 8.015c.166-.34.414-.697.758-1.037.02.348.053.67.098.973.083.56.207 1.048.341 1.477a3.41 3.41 0 0 1-.674.227c-.429.092-.588.019-.614.006l-.004-.001c-.162-.193-.329-.774.095-1.645Zm4.498-2.562a6.362 6.362 0 0 1-1.568 2.73 7.763 7.763 0 0 1-.095-.525 10.294 10.294 0 0 1-.088-1.904c.033-.013.067-.024.1-.036l1.651-.265Zm0 0-1.651.265c.602-.212 1.155-.29 1.651-.265ZM7.536 6.29a6.342 6.342 0 0 0-4.456.331 1 1 0 0 0 .848 1.811 4.342 4.342 0 0 1 3.049-.222c.364.107.568.248.69.37.12.123.203.27.257.454.067.225.087.446.09.69a8.195 8.195 0 0 0-.555-.117c-1.146-.199-2.733-.215-4.262.64-1.271.713-1.796 2.168-1.682 3.448.12 1.326.94 2.679 2.572 3.136 1.48.414 2.913-.045 3.877-.507l.08-.04a1 1 0 0 0 1.96-.281V10.5c0-.053.002-.12.005-.2.012-.417.034-1.16-.168-1.838a3.043 3.043 0 0 0-.755-1.29c-.394-.398-.91-.694-1.547-.881h-.003Zm-.419 5.288c.344.06.647.143.887.222v2.197a7.021 7.021 0 0 1-.905.524c-.792.38-1.682.605-2.473.384-.698-.195-1.06-.742-1.119-1.389-.062-.693.243-1.286.667-1.523.987-.553 2.06-.569 2.943-.415Z"
|
||||
}
|
||||
|
||||
@@ -69,7 +69,8 @@ class Message < ApplicationRecord
|
||||
# [:external_created_at] : Can specify if the message was created at a different timestamp externally
|
||||
# [:external_error : Can specify if the message creation failed due to an error at external API
|
||||
store :content_attributes, accessors: [:submitted_email, :items, :submitted_values, :email, :in_reply_to, :deleted,
|
||||
:external_created_at, :story_sender, :story_id, :external_error], coder: JSON
|
||||
:external_created_at, :story_sender, :story_id, :external_error,
|
||||
:translations], coder: JSON
|
||||
|
||||
store :external_source_ids, accessors: [:slack], coder: JSON, prefix: :external_source_id
|
||||
|
||||
|
||||
@@ -91,3 +91,40 @@ dyte:
|
||||
},
|
||||
]
|
||||
visible_properties: ["organization_id"]
|
||||
google_translate:
|
||||
id: google_translate
|
||||
logo: googletranslate.png
|
||||
i18n_key: google_translate
|
||||
action: /google_translate
|
||||
hook_type: account
|
||||
allow_multiple_hooks: false
|
||||
settings_json_schema: {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"project_id": { "type": "string" },
|
||||
"credentials": { "type": "object" },
|
||||
},
|
||||
"required": ["project_id", "credentials"],
|
||||
"additionalProperties": false,
|
||||
}
|
||||
settings_form_schema: [
|
||||
{
|
||||
"label": "Google Cloud Project ID",
|
||||
"type": "text",
|
||||
"name": "project_id",
|
||||
"validation": "required",
|
||||
"validationName": "Project Id",
|
||||
},
|
||||
{
|
||||
"label": "Google Cloud Project Key File",
|
||||
"type": "textarea",
|
||||
"name": "credentials",
|
||||
"validation": "required|JSON",
|
||||
"validationName": "Credentials",
|
||||
"validation-messages": {
|
||||
"JSON": "Invalid JSON",
|
||||
"required": "Credentials is required"
|
||||
},
|
||||
},
|
||||
]
|
||||
visible_properties: ['project_id']
|
||||
|
||||
@@ -171,6 +171,9 @@ en:
|
||||
fullcontact:
|
||||
name: "Fullcontact"
|
||||
description: "FullContact integration helps to enrich visitor profiles. Identify the users as soon as they share their email address and offer them tailored customer service. Connect your FullContact to your account by sharing the FullContact API Key."
|
||||
google_translate:
|
||||
name: "Google Translate"
|
||||
description: "Make it easier for agents to translate messages by adding a Google Translate Integration. Google translate helps to identify the language automatically and convert it to the language chosen by the agent/account admin."
|
||||
public_portal:
|
||||
search:
|
||||
search_placeholder: Search for article by title or body...
|
||||
|
||||
@@ -74,7 +74,11 @@ Rails.application.routes.draw do
|
||||
post :filter
|
||||
end
|
||||
scope module: :conversations do
|
||||
resources :messages, only: [:index, :create, :destroy]
|
||||
resources :messages, only: [:index, :create, :destroy] do
|
||||
member do
|
||||
post :translate
|
||||
end
|
||||
end
|
||||
resources :assignments, only: [:create]
|
||||
resources :labels, only: [:create, :index]
|
||||
resource :participants, only: [:show, :create, :update, :destroy]
|
||||
|
||||
30
lib/integrations/google_translate/processor_service.rb
Normal file
30
lib/integrations/google_translate/processor_service.rb
Normal file
@@ -0,0 +1,30 @@
|
||||
class Integrations::GoogleTranslate::ProcessorService
|
||||
pattr_initialize [:message!, :target_language!]
|
||||
|
||||
def perform
|
||||
return if message.content.blank?
|
||||
return if hook.blank?
|
||||
|
||||
response = client.translate_text(
|
||||
contents: [message.content],
|
||||
target_language_code: target_language,
|
||||
parent: "projects/#{hook.settings['project_id']}"
|
||||
)
|
||||
|
||||
return if response.translations.first.blank?
|
||||
|
||||
response.translations.first.translated_text
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def hook
|
||||
@hook ||= message.account.hooks.find_by(app_id: 'google_translate')
|
||||
end
|
||||
|
||||
def client
|
||||
@client ||= Google::Cloud::Translate.translation_service do |config|
|
||||
config.credentials = hook.settings['credentials']
|
||||
end
|
||||
end
|
||||
end
|
||||
BIN
public/dashboard/images/integrations/googletranslate.png
Normal file
BIN
public/dashboard/images/integrations/googletranslate.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.9 KiB |
Reference in New Issue
Block a user