feat: Add Google Translate API Integration (#6454)

This commit is contained in:
Pranav Raj S
2023-02-15 20:50:45 -08:00
committed by GitHub
parent c12bdc8350
commit 80784e3cab
17 changed files with 229 additions and 6 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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();

View File

@@ -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>

View File

@@ -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"
}
}

View File

@@ -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>

View File

@@ -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;

View File

@@ -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
}
},
};

View File

@@ -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) {

View File

@@ -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"
}

View File

@@ -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

View File

@@ -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']

View File

@@ -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...

View File

@@ -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]

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB