polylingo_chat 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +432 -0
  3. data/lib/generators/polylingo_chat/install/install_generator.rb +107 -0
  4. data/lib/generators/polylingo_chat/install/templates/README +48 -0
  5. data/lib/generators/polylingo_chat/install/templates/channels/application_cable/channel.rb +4 -0
  6. data/lib/generators/polylingo_chat/install/templates/channels/application_cable/connection.rb +30 -0
  7. data/lib/generators/polylingo_chat/install/templates/channels/polylingo_chat_channel.rb +15 -0
  8. data/lib/generators/polylingo_chat/install/templates/create_conversations.rb +9 -0
  9. data/lib/generators/polylingo_chat/install/templates/create_messages.rb +13 -0
  10. data/lib/generators/polylingo_chat/install/templates/create_participants.rb +12 -0
  11. data/lib/generators/polylingo_chat/install/templates/initializer.rb +24 -0
  12. data/lib/generators/polylingo_chat/install/templates/javascript/channels/consumer.js +15 -0
  13. data/lib/generators/polylingo_chat/install/templates/javascript/channels/index.js +2 -0
  14. data/lib/generators/polylingo_chat/install/templates/javascript/chat.js +86 -0
  15. data/lib/generators/polylingo_chat/install/templates/models/conversation.rb +7 -0
  16. data/lib/generators/polylingo_chat/install/templates/models/message.rb +14 -0
  17. data/lib/generators/polylingo_chat/install/templates/models/participant.rb +6 -0
  18. data/lib/generators/polylingo_chat/install_generator.rb +38 -0
  19. data/lib/generators/polylingo_chat/templates/INSTALL_README.md +124 -0
  20. data/lib/generators/polylingo_chat/templates/chat_channel_example.js +18 -0
  21. data/lib/generators/polylingo_chat/templates/create_polyglot_conversations.rb +9 -0
  22. data/lib/generators/polylingo_chat/templates/create_polyglot_messages.rb +13 -0
  23. data/lib/generators/polylingo_chat/templates/create_polyglot_participants.rb +12 -0
  24. data/lib/generators/polylingo_chat/templates/models/conversation.rb +7 -0
  25. data/lib/generators/polylingo_chat/templates/models/message.rb +14 -0
  26. data/lib/generators/polylingo_chat/templates/models/participant.rb +6 -0
  27. data/lib/generators/polylingo_chat/templates/polyglot.rb +53 -0
  28. data/lib/generators/polylingo_chat/templates/polylingo_chat_channel.rb +19 -0
  29. data/lib/polylingo_chat/config.rb +26 -0
  30. data/lib/polylingo_chat/engine.rb +10 -0
  31. data/lib/polylingo_chat/railtie.rb +14 -0
  32. data/lib/polylingo_chat/realtime.rb +8 -0
  33. data/lib/polylingo_chat/translate_job.rb +63 -0
  34. data/lib/polylingo_chat/translator/anthropic_client.rb +85 -0
  35. data/lib/polylingo_chat/translator/base.rb +13 -0
  36. data/lib/polylingo_chat/translator/gemini_client.rb +88 -0
  37. data/lib/polylingo_chat/translator/openai_client.rb +92 -0
  38. data/lib/polylingo_chat/translator.rb +40 -0
  39. data/lib/polylingo_chat/version.rb +3 -0
  40. data/lib/polylingo_chat.rb +11 -0
  41. metadata +138 -0
@@ -0,0 +1,24 @@
1
+ PolylingoChat.configure do |config|
2
+ # API key for AI translation service
3
+ # Leave nil to use chat-only mode (no translation)
4
+ config.api_key = nil
5
+
6
+ # AI provider: :openai, :anthropic, or :gemini
7
+ config.provider = :anthropic
8
+
9
+ # Model to use for translation
10
+ # OpenAI: 'gpt-4-turbo' or 'gpt-3.5-turbo'
11
+ # Anthropic: 'claude-3-5-sonnet-20241022' or 'claude-3-haiku-20240307'
12
+ # Gemini: 'gemini-1.5-pro' or 'gemini-1.5-flash'
13
+ config.model = 'claude-3-5-sonnet-20241022'
14
+
15
+ # Default language for users without a preferred language
16
+ config.default_language = 'en'
17
+
18
+ # Enable async job processing (requires ActiveJob)
19
+ config.async = true
20
+
21
+ # Queue adapter (e.g., :solid_queue, :sidekiq, :async)
22
+ # Set to match your app's ActiveJob queue adapter
23
+ # config.queue_adapter = :solid_queue
24
+ end
@@ -0,0 +1,15 @@
1
+ // Action Cable provides the framework to deal with WebSockets in Rails.
2
+ // You can generate new channels where WebSocket features live using the `bin/rails generate channel` command.
3
+
4
+ import { createConsumer } from "@rails/actioncable"
5
+
6
+ // Pass user_id as a query parameter for authentication
7
+ const getWebSocketURL = () => {
8
+ const userId = window.currentUserId
9
+ if (userId) {
10
+ return `/cable?user_id=${userId}`
11
+ }
12
+ return "/cable"
13
+ }
14
+
15
+ export default createConsumer(getWebSocketURL())
@@ -0,0 +1,2 @@
1
+ // Import all the channels to be used by Action Cable
2
+ import "./consumer"
@@ -0,0 +1,86 @@
1
+ import consumer from "channels/consumer"
2
+
3
+ console.log("Chat.js loaded!")
4
+
5
+ document.addEventListener('turbo:load', () => {
6
+ console.log("Turbo loaded!")
7
+
8
+ const conversationId = window.conversationId
9
+ const currentUserId = window.currentUserId
10
+
11
+ console.log("Conversation ID:", conversationId)
12
+ console.log("Current User ID:", currentUserId)
13
+
14
+ if (!conversationId) {
15
+ console.log("No conversation ID found, skipping ActionCable subscription")
16
+ return
17
+ }
18
+
19
+ // Subscribe to the PolylinguoChatChannel
20
+ console.log("Subscribing to PolylinguoChatChannel...")
21
+ const subscription = consumer.subscriptions.create(
22
+ { channel: "PolylinguoChatChannel", conversation_id: conversationId },
23
+ {
24
+ connected() {
25
+ console.log("✓ Connected to PolylinguoChatChannel!")
26
+ },
27
+
28
+ disconnected() {
29
+ console.log("✗ Disconnected from PolylinguoChatChannel")
30
+ },
31
+
32
+ received(data) {
33
+ console.log("✓ Received message:", data)
34
+
35
+ const messagesContainer = document.getElementById('messages')
36
+ if (!messagesContainer) return
37
+
38
+ // Check if message already exists in DOM (prevent duplicates)
39
+ const existingMessage = messagesContainer.querySelector(`[data-message-id="${data.message_id}"]`)
40
+ if (existingMessage) {
41
+ console.log("Message already exists, skipping:", data.message_id)
42
+ return
43
+ }
44
+
45
+ // Add new message
46
+ const messageHtml = createMessageElement(data)
47
+ messagesContainer.insertAdjacentHTML('beforeend', messageHtml)
48
+
49
+ // Auto-scroll to bottom
50
+ messagesContainer.scrollTop = messagesContainer.scrollHeight
51
+ }
52
+ }
53
+ )
54
+
55
+ // Auto-scroll messages to bottom on page load
56
+ const messagesContainer = document.getElementById('messages')
57
+ if (messagesContainer) {
58
+ messagesContainer.scrollTop = messagesContainer.scrollHeight
59
+ }
60
+
61
+ function createMessageElement(data) {
62
+ const isCurrentUser = data.sender_id === currentUserId
63
+ const alignment = isCurrentUser ? 'justify-end' : 'justify-start'
64
+ const bgColor = isCurrentUser ? 'bg-blue-500 text-white' : 'bg-gray-200 text-gray-800'
65
+
66
+ const translatedBadge = data.translated ? '<p class="text-xs mt-1 opacity-75">✓ Translated</p>' : ''
67
+ const time = new Date().toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' })
68
+
69
+ return `
70
+ <div class="message flex ${alignment}" data-message-id="${data.message_id}">
71
+ <div class="${bgColor} rounded-lg px-4 py-2 max-w-xs">
72
+ <p class="text-xs font-semibold mb-1">${escapeHtml(data.sender_name || 'Unknown')}</p>
73
+ <p class="text-sm">${escapeHtml(data.message)}</p>
74
+ ${translatedBadge}
75
+ <p class="text-xs mt-1 opacity-75">${time}</p>
76
+ </div>
77
+ </div>
78
+ `
79
+ }
80
+
81
+ function escapeHtml(text) {
82
+ const div = document.createElement('div')
83
+ div.textContent = text
84
+ return div.innerHTML
85
+ }
86
+ })
@@ -0,0 +1,7 @@
1
+ class Conversation < ApplicationRecord
2
+ has_many :participants, dependent: :destroy
3
+ has_many :users, through: :participants
4
+ has_many :messages, dependent: :destroy
5
+
6
+ validates :title, length: { maximum: 255 }, allow_blank: true
7
+ end
@@ -0,0 +1,14 @@
1
+ class Message < ApplicationRecord
2
+ belongs_to :conversation
3
+ belongs_to :sender, class_name: 'User', foreign_key: 'sender_id'
4
+
5
+ validates :body, presence: true
6
+
7
+ after_create_commit :enqueue_translation_job
8
+
9
+ private
10
+
11
+ def enqueue_translation_job
12
+ PolylingoChat::TranslateJob.perform_later(id) if PolylingoChat.config.async
13
+ end
14
+ end
@@ -0,0 +1,6 @@
1
+ class Participant < ApplicationRecord
2
+ belongs_to :user
3
+ belongs_to :conversation
4
+
5
+ validates :user_id, uniqueness: { scope: :conversation_id }
6
+ end
@@ -0,0 +1,38 @@
1
+ require 'rails/generators'
2
+ module PolylingoChat
3
+ module Generators
4
+ class InstallGenerator < Rails::Generators::Base
5
+ source_root File.expand_path('templates', __dir__)
6
+
7
+ def copy_initializer
8
+ template 'polylingo_chat.rb', 'config/initializers/polylingo_chat.rb'
9
+ end
10
+
11
+ def copy_channel
12
+ template 'polylingo_chat_chat_channel.rb', 'app/channels/polylingo_chat_chat_channel.rb'
13
+ end
14
+
15
+ def copy_js_example
16
+ template 'chat_channel_example.js', 'app/javascript/channels/polylingo_chat_chat_channel.js'
17
+ end
18
+
19
+ def copy_models
20
+ template 'models/conversation.rb', 'app/models/conversation.rb'
21
+ template 'models/participant.rb', 'app/models/participant.rb'
22
+ template 'models/message.rb', 'app/models/message.rb'
23
+ end
24
+
25
+ def copy_migrations
26
+ migration_template 'create_polylingo_chat_conversations.rb', 'db/migrate/create_polylingo_chat_conversations.rb'
27
+ migration_template 'create_polylingo_chat_participants.rb', 'db/migrate/create_polylingo_chat_participants.rb'
28
+ migration_template 'create_polylingo_chat_messages.rb', 'db/migrate/create_polylingo_chat_messages.rb'
29
+ rescue => e
30
+ say_status('warning', 'Skipping migrations: ensure you add your own Message/Conversation models or run generator again with --migrate', :yellow)
31
+ end
32
+
33
+ def show_readme
34
+ readme 'INSTALL_README.md' if behavior == :invoke
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,124 @@
1
+ ## PolylingoChat Installation Complete!
2
+
3
+ ### Next Steps:
4
+
5
+ **PolylingoChat can work in TWO modes:**
6
+ 1. **Chat-Only Mode:** Real-time chat without translation (no API key needed)
7
+ 2. **With Translation:** AI-powered translation (API key required)
8
+
9
+ ---
10
+
11
+ ### 1. Run migrations
12
+ ```bash
13
+ bin/rails db:migrate
14
+ ```
15
+
16
+ ### 2. Configure ActiveJob
17
+
18
+ ```ruby
19
+ # config/application.rb or config/environments/production.rb
20
+ config.active_job.queue_adapter = :sidekiq # or :solid_queue, :delayed_job, :async
21
+ ```
22
+
23
+ ### 3. Install your background job processor
24
+
25
+ **Sidekiq:**
26
+ ```ruby
27
+ # Gemfile
28
+ gem 'sidekiq'
29
+ ```
30
+ Then: `bundle install && bundle exec sidekiq`
31
+
32
+ **Solid Queue:**
33
+ ```ruby
34
+ # Gemfile
35
+ gem 'solid_queue'
36
+ ```
37
+ Then: `bundle install && bin/rails solid_queue:install && bin/rails db:migrate && bin/rails solid_queue:start`
38
+
39
+ **Delayed Job:**
40
+ ```ruby
41
+ # Gemfile
42
+ gem 'delayed_job_active_record'
43
+ ```
44
+ Then: `bundle install && bin/rails generate delayed_job:active_record && bin/rails db:migrate && bin/rails jobs:work`
45
+
46
+ ---
47
+
48
+ ### 4. Configure PolylingoChat
49
+
50
+ Edit `config/initializers/polylingo_chat.rb`:
51
+
52
+ **Option A: Chat-Only (No Translation)**
53
+ ```ruby
54
+ PolylingoChat.configure do |config|
55
+ # Leave api_key as nil for chat-only mode
56
+ config.api_key = nil
57
+ config.queue_adapter = :sidekiq
58
+ config.async = true
59
+ end
60
+ ```
61
+
62
+ **Option B: With AI Translation**
63
+ ```ruby
64
+ PolylingoChat.configure do |config|
65
+ # Enable translation
66
+ config.provider = :openai # or :anthropic, :gemini
67
+ config.api_key = ENV['POLYGLOT_API_KEY']
68
+ config.model = 'gpt-4o-mini'
69
+
70
+ config.queue_adapter = :sidekiq
71
+ config.default_language = 'en'
72
+ config.cache_store = Rails.cache
73
+ end
74
+ ```
75
+
76
+ ---
77
+
78
+ ### 5. (Optional) Add preferred_language to User
79
+
80
+ **Only needed if using translation:**
81
+ ```bash
82
+ bin/rails generate migration AddPreferredLanguageToUsers preferred_language:string
83
+ bin/rails db:migrate
84
+ ```
85
+
86
+ ---
87
+
88
+ ### 6. Get API Key (Optional)
89
+
90
+ **Only if you want translation:**
91
+ - OpenAI: https://platform.openai.com/api-keys
92
+ - Anthropic: https://console.anthropic.com/
93
+ - Gemini: https://ai.google.dev/
94
+
95
+ ```bash
96
+ # .env
97
+ POLYGLOT_API_KEY=your-key-here
98
+ ```
99
+
100
+ ---
101
+
102
+ ### Testing
103
+
104
+ ```ruby
105
+ conversation = Conversation.create!(title: "Test")
106
+ Participant.create!(conversation: conversation, user: user1)
107
+ Participant.create!(conversation: conversation, user: user2)
108
+
109
+ Message.create!(
110
+ conversation: conversation,
111
+ sender: user1,
112
+ body: "Hello!"
113
+ )
114
+ ```
115
+
116
+ - **Without API key:** Message sent as-is
117
+ - **With API key:** Message translated to each user's language
118
+
119
+ ---
120
+
121
+ ### Need Help?
122
+
123
+ - 📖 Documentation: https://github.com/shoaibmalik786/polylingo_chat
124
+ - 🐛 Issues: https://github.com/shoaibmalik786/polylingo_chat/issues
@@ -0,0 +1,18 @@
1
+ import consumer from "@rails/actioncable"
2
+
3
+ const cable = consumer.createConsumer();
4
+
5
+ export default function subscribe(conversationId, handleReceive) {
6
+ const subscription = cable.subscriptions.create({ channel: 'PolylinguoChatChannel', conversation_id: conversationId }, {
7
+ connected() {},
8
+ disconnected() {},
9
+ received(data) {
10
+ handleReceive(data)
11
+ },
12
+ sendMessage(text, conversationId) {
13
+ this.perform('receive', { message: text, conversation_id: conversationId });
14
+ }
15
+ });
16
+
17
+ return subscription;
18
+ }
@@ -0,0 +1,9 @@
1
+ class CreatePolylingoChatConversations < ActiveRecord::Migration[6.0]
2
+ def change
3
+ create_table :conversations do |t|
4
+ t.string :title
5
+ t.boolean :private, default: true
6
+ t.timestamps
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,13 @@
1
+ class CreatePolylingoChatMessages < ActiveRecord::Migration[6.0]
2
+ def change
3
+ create_table :messages do |t|
4
+ t.references :sender, null: false, foreign_key: { to_table: :users }
5
+ t.references :conversation, null: false, foreign_key: true
6
+ t.text :body
7
+ t.string :language
8
+ t.text :translated_body
9
+ t.boolean :translated, default: false
10
+ t.timestamps
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,12 @@
1
+ class CreatePolylingoChatParticipants < ActiveRecord::Migration[6.0]
2
+ def change
3
+ create_table :participants do |t|
4
+ t.references :user, null: false, foreign_key: true
5
+ t.references :conversation, null: false, foreign_key: true
6
+ t.string :role
7
+ t.timestamps
8
+ end
9
+
10
+ add_index :participants, [:user_id, :conversation_id], unique: true
11
+ end
12
+ end
@@ -0,0 +1,7 @@
1
+ class Conversation < ApplicationRecord
2
+ has_many :participants, dependent: :destroy
3
+ has_many :users, through: :participants
4
+ has_many :messages, dependent: :destroy
5
+
6
+ validates :title, length: { maximum: 255 }, allow_blank: true
7
+ end
@@ -0,0 +1,14 @@
1
+ class Message < ApplicationRecord
2
+ belongs_to :conversation
3
+ belongs_to :sender, class_name: 'User', foreign_key: 'sender_id'
4
+
5
+ validates :body, presence: true
6
+
7
+ after_create_commit :enqueue_translation_job
8
+
9
+ private
10
+
11
+ def enqueue_translation_job
12
+ PolylingoChat::TranslateJob.perform_later(id) if PolylingoChat.config.async
13
+ end
14
+ end
@@ -0,0 +1,6 @@
1
+ class Participant < ApplicationRecord
2
+ belongs_to :user
3
+ belongs_to :conversation
4
+
5
+ validates :user_id, uniqueness: { scope: :conversation_id }
6
+ end
@@ -0,0 +1,53 @@
1
+ # PolylingoChat Configuration
2
+ # For more information, see: https://github.com/shoaibmalik786/polylingo_chat
3
+
4
+ PolylingoChat.configure do |config|
5
+ # ========================================
6
+ # AI Provider Configuration (OPTIONAL)
7
+ # ========================================
8
+
9
+ # Translation is OPTIONAL! If you don't set an API key, PolylingoChat works as a
10
+ # real-time chat engine without translation (messages sent as-is).
11
+
12
+ # To enable translation, choose your AI provider: :openai, :anthropic, or :gemini
13
+ config.provider = :openai
14
+
15
+ # Set your API key (leave nil to use chat-only mode without translation)
16
+ config.api_key = ENV['POLYGLOT_API_KEY'] # or set to nil for chat-only mode
17
+
18
+ # Choose your model based on provider:
19
+ # OpenAI: 'gpt-4o-mini', 'gpt-4o', 'gpt-3.5-turbo'
20
+ # Anthropic: 'claude-3-5-sonnet-20241022', 'claude-3-5-haiku-20241022', 'claude-3-opus-20240229'
21
+ # Gemini: 'gemini-1.5-flash', 'gemini-1.5-pro'
22
+ config.model = 'gpt-4o-mini'
23
+
24
+ # ========================================
25
+ # Background Job Configuration
26
+ # ========================================
27
+
28
+ # Specify which ActiveJob adapter you're using (OPTIONAL, for documentation purposes)
29
+ # You still need to configure ActiveJob in your Rails app as normal
30
+ # Options: :sidekiq, :solid_queue, :delayed_job, :async, :inline, etc.
31
+ #
32
+ # Example ActiveJob configuration (in config/application.rb or config/environments/*.rb):
33
+ # config.active_job.queue_adapter = :sidekiq
34
+ #
35
+ # Then tell PolylingoChat which adapter you're using:
36
+ config.queue_adapter = :sidekiq # Change this to match your ActiveJob setup
37
+
38
+ # Enable/disable async processing
39
+ config.async = true
40
+
41
+ # ========================================
42
+ # Translation Configuration
43
+ # ========================================
44
+
45
+ # Default language for translations (ISO 639-1 code)
46
+ config.default_language = 'en'
47
+
48
+ # Enable caching for translations (recommended for production)
49
+ config.cache_store = Rails.cache
50
+
51
+ # Request timeout in seconds
52
+ config.timeout = 15
53
+ end
@@ -0,0 +1,19 @@
1
+ class PolylinguoChatChannel < ApplicationCable::Channel
2
+ def subscribed
3
+ # stream for the current user
4
+ stream_from "polylingo_chat_recipient_#{current_user.id}"
5
+ end
6
+
7
+ def receive(data)
8
+ # data: { message: 'Hola', conversation_id: 1 }
9
+ message_text = data['message']
10
+ conversation = Conversation.find(data['conversation_id'])
11
+
12
+ msg = Message.create!(conversation: conversation, sender: current_user, body: message_text, language: current_user.preferred_language)
13
+ # enqueue translation job
14
+ PolylingoChat::TranslateJob.perform_async(msg.id)
15
+
16
+ # broadcast original to sender's stream if you want
17
+ ActionCable.server.broadcast("polylingo_chat_recipient_#{current_user.id}", { message: message_text, original: message_text, sender_id: current_user.id, message_id: msg.id, original:true })
18
+ end
19
+ end
@@ -0,0 +1,26 @@
1
+ module PolylingoChat
2
+ class Config
3
+ attr_accessor :provider, :api_key, :model, :default_language, :cache_store, :async, :timeout, :queue_adapter
4
+
5
+ def initialize
6
+ @provider = :openai
7
+ @api_key = ENV['POLYGLOT_API_KEY']
8
+ @model = 'gpt-4o-mini'
9
+ @default_language = 'en'
10
+ @cache_store = nil
11
+ @async = true
12
+ @timeout = 15
13
+ @queue_adapter = nil # Optional: Specify which ActiveJob adapter you're using
14
+ end
15
+ end
16
+
17
+ def self.configure
18
+ @config ||= Config.new
19
+ yield @config if block_given?
20
+ @config
21
+ end
22
+
23
+ def self.config
24
+ @config ||= Config.new
25
+ end
26
+ end
@@ -0,0 +1,10 @@
1
+ require 'rails/engine'
2
+ module PolylingoChat
3
+ class Engine < ::Rails::Engine
4
+ isolate_namespace PolylingoChat
5
+
6
+ config.generators do |g|
7
+ g.test_framework :rspec
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,14 @@
1
+ require 'rails/railtie'
2
+
3
+ module PolylingoChat
4
+ class Railtie < Rails::Railtie
5
+ # PolylingoChat Railtie - currently just for initialization hooks
6
+ # The queue_adapter config is informational - users configure ActiveJob themselves
7
+
8
+ initializer 'polylingo_chat.log_configuration', after: :load_config_initializers do
9
+ if PolylingoChat.config.queue_adapter
10
+ Rails.logger.info "PolylingoChat: Using #{PolylingoChat.config.queue_adapter} for background jobs"
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,8 @@
1
+ module PolylingoChat
2
+ module Realtime
3
+ def self.channel_template
4
+ # returns a string of the channel implementation for host app generator
5
+ File.read(File.expand_path('../../templates/polylingo_chat_chat_channel.rb', __dir__))
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,63 @@
1
+ module PolylingoChat
2
+ class TranslateJob < ActiveJob::Base
3
+ queue_as :polylingo_chat_translations
4
+
5
+ def perform(message_id)
6
+ # Host app must implement Message model with associations matching spec
7
+ message = ::Message.find_by(id: message_id)
8
+ return unless message
9
+
10
+ conversation = message.conversation
11
+ return unless conversation
12
+
13
+ recipients = conversation.users.where.not(id: message.sender_id)
14
+
15
+ # Check if translation is enabled (API key present)
16
+ translation_enabled = PolylingoChat.config.api_key.present?
17
+
18
+ recipients.each do |recipient|
19
+ if translation_enabled
20
+ # Translation enabled - translate message
21
+ target_lang = recipient.preferred_language || PolylingoChat.config.default_language
22
+ source_lang = PolylingoChat::Translator.detect_language(message.body)
23
+ context = conversation.messages.order(created_at: :asc).last(20).pluck(:body).join("") rescue nil
24
+
25
+ translated = PolylingoChat::Translator.translate(text: message.body, from: source_lang, to: target_lang, context: context)
26
+ # If translator returns a Future-like, wait
27
+ translated = translated.value if translated.respond_to?(:value)
28
+ else
29
+ # No API key - just use original message (chat-only mode)
30
+ translated = message.body
31
+ end
32
+
33
+ # Broadcast to recipient using ActionCable
34
+ begin
35
+ # Broadcast to user-specific channel
36
+ ActionCable.server.broadcast("polylingo_chat_recipient_#{recipient.id}", {
37
+ message: translated,
38
+ original: message.body,
39
+ message_id: message.id,
40
+ sender_id: message.sender_id,
41
+ sender_name: message.sender.name,
42
+ translated: translation_enabled
43
+ })
44
+
45
+ # Also broadcast to conversation channel for demo/group chat
46
+ ActionCable.server.broadcast("conversation_#{conversation.id}", {
47
+ message: translated,
48
+ original: message.body,
49
+ message_id: message.id,
50
+ sender_id: message.sender_id,
51
+ sender_name: message.sender.name,
52
+ translated: translation_enabled,
53
+ recipient_id: recipient.id
54
+ })
55
+ rescue => e
56
+ # ignore broadcast errors
57
+ end
58
+ end
59
+
60
+ message.update(translated: translation_enabled) rescue nil
61
+ end
62
+ end
63
+ end