diff --git a/config/integration/apps.yml b/config/integration/apps.yml index dd5c722a4..1a45cc098 100644 --- a/config/integration/apps.yml +++ b/config/integration/apps.yml @@ -91,6 +91,7 @@ dialogflow: 'project_id': { 'type': 'string' }, 'credentials': { 'type': 'object' }, 'region': { 'type': 'string' }, + 'language_code': { 'type': 'string' }, }, 'required': ['project_id', 'credentials'], 'additionalProperties': false, @@ -126,8 +127,40 @@ dialogflow: { 'label': 'EU-W2 - London, England', 'value': 'europe-west2' }, ], }, + { + 'label': 'Language Code', + 'type': 'select', + 'name': 'language_code', + 'default': 'en-US', + 'help': 'Language code for Dialogflow agent. Use "auto" to detect from contact language.', + 'options': [ + { 'label': 'Auto-detect from contact', 'value': 'auto' }, + { 'label': 'English (US)', 'value': 'en-US' }, + { 'label': 'English (UK)', 'value': 'en-GB' }, + { 'label': 'Spanish (Spain)', 'value': 'es-ES' }, + { 'label': 'Spanish (Latin America)', 'value': 'es-419' }, + { 'label': 'French', 'value': 'fr-FR' }, + { 'label': 'German', 'value': 'de-DE' }, + { 'label': 'Portuguese (Brazil)', 'value': 'pt-BR' }, + { 'label': 'Portuguese (Portugal)', 'value': 'pt-PT' }, + { 'label': 'Italian', 'value': 'it-IT' }, + { 'label': 'Japanese', 'value': 'ja-JP' }, + { 'label': 'Korean', 'value': 'ko-KR' }, + { 'label': 'Chinese (Simplified)', 'value': 'zh-CN' }, + { 'label': 'Chinese (Traditional)', 'value': 'zh-TW' }, + { 'label': 'Hindi', 'value': 'hi-IN' }, + { 'label': 'Arabic', 'value': 'ar' }, + { 'label': 'Russian', 'value': 'ru-RU' }, + { 'label': 'Dutch', 'value': 'nl-NL' }, + { 'label': 'Polish', 'value': 'pl-PL' }, + { 'label': 'Turkish', 'value': 'tr-TR' }, + { 'label': 'Thai', 'value': 'th-TH' }, + { 'label': 'Vietnamese', 'value': 'vi-VN' }, + { 'label': 'Indonesian', 'value': 'id-ID' }, + ], + }, ] - visible_properties: ['project_id', 'region'] + visible_properties: ['project_id', 'region', 'language_code'] google_translate: id: google_translate logo: google-translate.png diff --git a/lib/integrations/dialogflow/processor_service.rb b/lib/integrations/dialogflow/processor_service.rb index 578a277a2..77783c7e8 100644 --- a/lib/integrations/dialogflow/processor_service.rb +++ b/lib/integrations/dialogflow/processor_service.rb @@ -1,6 +1,51 @@ require 'google/cloud/dialogflow/v2' class Integrations::Dialogflow::ProcessorService < Integrations::BotProcessorService + SUPPORTED_LANGUAGE_CODES = %w[ + ar + en-US + en-GB + es-ES + es-419 + fr-FR + de-DE + pt-BR + pt-PT + it-IT + ja-JP + ko-KR + zh-CN + zh-TW + hi-IN + ru-RU + nl-NL + pl-PL + tr-TR + th-TH + vi-VN + id-ID + ].freeze + AUTO_LANGUAGE_CODE_MAP = { + 'ar' => 'ar', + 'de' => 'de-DE', + 'en' => 'en-US', + 'es' => 'es-ES', + 'fr' => 'fr-FR', + 'hi' => 'hi-IN', + 'id' => 'id-ID', + 'it' => 'it-IT', + 'ja' => 'ja-JP', + 'ko' => 'ko-KR', + 'nl' => 'nl-NL', + 'pl' => 'pl-PL', + 'pt' => 'pt-BR', + 'ru' => 'ru-RU', + 'th' => 'th-TH', + 'tr' => 'tr-TR', + 'vi' => 'vi-VN', + 'zh' => 'zh-CN' + }.freeze + pattr_initialize [:event_name!, :hook!, :event_data!] private @@ -84,7 +129,7 @@ class Integrations::Dialogflow::ProcessorService < Integrations::BotProcessorSer def detect_intent(session_id, message) client = ::Google::Cloud::Dialogflow::V2::Sessions::Client.new session = build_session_path(session_id) - query_input = { text: { text: message, language_code: 'en-US' } } + query_input = { text: { text: message, language_code: dialogflow_language_code } } client.detect_intent session: session, query_input: query_input end @@ -98,4 +143,30 @@ class Integrations::Dialogflow::ProcessorService < Integrations::BotProcessorSer "projects/#{project_id}/locations/#{region}/agent/sessions/#{session_id}" end end + + def dialogflow_language_code + configured_language = hook.settings['language_code'].to_s.strip + return 'en-US' if configured_language.blank? + return configured_language if configured_language != 'auto' + + normalized_contact_language_code(conversation&.contact&.additional_attributes&.dig('language_code')) || 'en-US' + end + + def normalized_contact_language_code(language_code) + canonicalized_language_code = canonical_language_code(language_code) + return if canonicalized_language_code.blank? + return canonicalized_language_code if SUPPORTED_LANGUAGE_CODES.include?(canonicalized_language_code) + + AUTO_LANGUAGE_CODE_MAP[canonicalized_language_code] || AUTO_LANGUAGE_CODE_MAP[canonicalized_language_code.split('-', 2).first] + end + + def canonical_language_code(language_code) + normalized_language_code = language_code.to_s.tr('_', '-').strip + return if normalized_language_code.blank? + + language, region = normalized_language_code.split('-', 2) + return language.downcase if region.blank? + + "#{language.downcase}-#{region.upcase}" + end end diff --git a/spec/factories/integrations/hooks.rb b/spec/factories/integrations/hooks.rb index 85517335a..e02bc2409 100644 --- a/spec/factories/integrations/hooks.rb +++ b/spec/factories/integrations/hooks.rb @@ -9,7 +9,7 @@ FactoryBot.define do trait :dialogflow do app_id { 'dialogflow' } - settings { { project_id: 'test', credentials: {}, region: 'global' } } + settings { { project_id: 'test', credentials: {}, region: 'global', language_code: 'en-US' } } end trait :dyte do diff --git a/spec/lib/integrations/dialogflow/processor_service_spec.rb b/spec/lib/integrations/dialogflow/processor_service_spec.rb index 3160f74d3..25803815b 100644 --- a/spec/lib/integrations/dialogflow/processor_service_spec.rb +++ b/spec/lib/integrations/dialogflow/processor_service_spec.rb @@ -236,4 +236,141 @@ describe Integrations::Dialogflow::ProcessorService do end end end + + describe 'language_code configuration' do + let(:processor) { described_class.new(event_name: event_name, hook: hook, event_data: event_data) } + let(:mock_client) { instance_double(Google::Cloud::Dialogflow::V2::Sessions::Client) } + + before do + allow(Google::Cloud::Dialogflow::V2::Sessions::Client).to receive(:new).and_return(mock_client) + end + + context 'when language_code is configured' do + it 'uses the configured language code' do + hook.update(settings: { 'project_id' => 'test-project', 'credentials' => {}, 'language_code' => 'es-ES' }) + + expect(processor.send(:dialogflow_language_code)).to eq('es-ES') + end + + it 'passes the configured language code to detect_intent' do + hook.update(settings: { 'project_id' => 'test-project', 'credentials' => {}, 'language_code' => 'fr-FR' }) + + expect(mock_client).to receive(:detect_intent).with( + session: 'projects/test-project/agent/sessions/test-session', + query_input: { text: { text: 'Hello', language_code: 'fr-FR' } } + ) + + processor.send(:detect_intent, 'test-session', 'Hello') + end + end + + context 'when language_code is set to auto' do + it 'uses contact language_code from additional_attributes' do + hook.update(settings: { 'project_id' => 'test-project', 'credentials' => {}, 'language_code' => 'auto' }) + conversation.contact.update(additional_attributes: { 'language_code' => 'pt-BR' }) + + expect(processor.send(:dialogflow_language_code)).to eq('pt-BR') + end + + it 'normalizes short contact language codes to supported Dialogflow locales' do + hook.update(settings: { 'project_id' => 'test-project', 'credentials' => {}, 'language_code' => 'auto' }) + conversation.contact.update(additional_attributes: { 'language_code' => 'en' }) + + expect(processor.send(:dialogflow_language_code)).to eq('en-US') + end + + it 'maps spanish short codes to the preferred locale' do + hook.update(settings: { 'project_id' => 'test-project', 'credentials' => {}, 'language_code' => 'auto' }) + conversation.contact.update(additional_attributes: { 'language_code' => 'es' }) + + expect(processor.send(:dialogflow_language_code)).to eq('es-ES') + end + + it 'maps unsupported spanish regional variants through the base language' do + hook.update(settings: { 'project_id' => 'test-project', 'credentials' => {}, 'language_code' => 'auto' }) + conversation.contact.update(additional_attributes: { 'language_code' => 'es-MX' }) + + expect(processor.send(:dialogflow_language_code)).to eq('es-ES') + end + + it 'maps portuguese short codes to the preferred locale' do + hook.update(settings: { 'project_id' => 'test-project', 'credentials' => {}, 'language_code' => 'auto' }) + conversation.contact.update(additional_attributes: { 'language_code' => 'pt' }) + + expect(processor.send(:dialogflow_language_code)).to eq('pt-BR') + end + + it 'maps unsupported portuguese regional variants through the base language' do + hook.update(settings: { 'project_id' => 'test-project', 'credentials' => {}, 'language_code' => 'auto' }) + conversation.contact.update(additional_attributes: { 'language_code' => 'pt-AO' }) + + expect(processor.send(:dialogflow_language_code)).to eq('pt-BR') + end + + it 'maps chinese short codes to the preferred locale' do + hook.update(settings: { 'project_id' => 'test-project', 'credentials' => {}, 'language_code' => 'auto' }) + conversation.contact.update(additional_attributes: { 'language_code' => 'zh' }) + + expect(processor.send(:dialogflow_language_code)).to eq('zh-CN') + end + + it 'maps arabic short codes to the preferred locale' do + hook.update(settings: { 'project_id' => 'test-project', 'credentials' => {}, 'language_code' => 'auto' }) + conversation.contact.update(additional_attributes: { 'language_code' => 'ar' }) + + expect(processor.send(:dialogflow_language_code)).to eq('ar') + end + + it 'normalizes contact language formatting before checking supported locales' do + hook.update(settings: { 'project_id' => 'test-project', 'credentials' => {}, 'language_code' => 'auto' }) + conversation.contact.update(additional_attributes: { 'language_code' => 'pt_br' }) + + expect(processor.send(:dialogflow_language_code)).to eq('pt-BR') + end + + it 'falls back to en-US for unsupported contact language codes' do + hook.update(settings: { 'project_id' => 'test-project', 'credentials' => {}, 'language_code' => 'auto' }) + conversation.contact.update(additional_attributes: { 'language_code' => 'xx' }) + + expect(processor.send(:dialogflow_language_code)).to eq('en-US') + end + + it 'falls back to en-US when contact has no language' do + hook.update(settings: { 'project_id' => 'test-project', 'credentials' => {}, 'language_code' => 'auto' }) + conversation.contact.update(additional_attributes: {}) + + expect(processor.send(:dialogflow_language_code)).to eq('en-US') + end + end + + context 'when language_code is not configured' do + before do + conversation.contact.update(additional_attributes: { 'language_code' => 'pt-BR' }) + end + + it 'falls back to en-US' do + hook.update(settings: { 'project_id' => 'test-project', 'credentials' => {} }) + + expect(processor.send(:dialogflow_language_code)).to eq('en-US') + end + end + + context 'when language_code is empty or blank' do + before do + conversation.contact.update(additional_attributes: { 'language_code' => 'pt-BR' }) + end + + it 'falls back to en-US for empty string' do + hook.update(settings: { 'project_id' => 'test-project', 'credentials' => {}, 'language_code' => '' }) + + expect(processor.send(:dialogflow_language_code)).to eq('en-US') + end + + it 'falls back to en-US for whitespace-only string' do + hook.update(settings: { 'project_id' => 'test-project', 'credentials' => {}, 'language_code' => ' ' }) + + expect(processor.send(:dialogflow_language_code)).to eq('en-US') + end + end + end end