PolylingoChat is a plug-and-play Rails Engine that adds real-time chat to your Rails app. Use it as a chat-only engine or enable AI-powered translation for multilingual conversations.
Perfect for marketplaces, SaaS apps, CRMs, support systems, or global communities.
- 🔥 Real-time chat using ActionCable with Solid Cable (database-backed)
- 🚀 One-command installer — everything set up automatically
- 🌍 Optional AI translation - works with or without translation
- ⚙️ Multi-provider support — OpenAI, Anthropic Claude, or Google Gemini
- 🧩 Rails Engine — mounts directly inside your app
- 📱 API-only mode — works with Rails API applications (auto-detected)
- 👥 Polymorphic associations — supports multiple participant types (User, Vendor, Customer, etc.)
- 🔌 Works with any ActiveJob backend (Sidekiq, Solid Queue, Delayed Job, etc.)
- 🔐 Secure & scoped ActionCable channels
- 🧱 Extendable architecture (custom UI, custom providers, custom storage)
- 💾 Database-backed translation caching - translations stored and reused (zero API costs on retrieval)
- 🧪 RSpec testing framework included
- 🗄️ No Redis required — uses Solid Cable for WebSocket persistence
- Ruby >= 2.7.0
- Rails >= 6.0
- A background job processor (Sidekiq, Solid Queue, or Delayed Job)
- Optional: AI API key (only if you want translation)
Note: PolylingoChat uses Solid Cable (database-backed ActionCable) by default, so Redis is optional.
# Gemfile
gem "polylingo_chat", github: "AdwareTechnologies/polylingo_chat"
# Add a background job processor
gem 'sidekiq' # or 'solid_queue' or 'delayed_job_active_record'bundle install
bin/rails generate polylingo_chat:install
bin/rails db:migrateThe installer automatically:
- ✅ Creates migrations for prefixed tables (
polylingo_chat_conversations,polylingo_chat_participants,polylingo_chat_messages,polylingo_chat_message_translations) - ✅ Generates namespaced models in
app/models/polylingo_chat/directory - ✅ Generates namespaced controllers in
app/controllers/polylingo_chat/directory - ✅ Sets up ActionCable channels (PolylinguoChatChannel) for full-stack apps
- ✅ Creates JavaScript files for real-time chat (full-stack only)
- ✅ Configures Solid Cable in
config/cable.yml(full-stack only) - ✅ Downloads ActionCable ESM module to
vendor/javascript(full-stack only) - ✅ Updates
config/routes.rbwith namespaced routes - ✅ Updates
config/importmap.rbwith required pins (full-stack only) - ✅ Updates
app/javascript/application.jswith chat import (full-stack only) - ✅ Creates
config/initializers/polylingo_chat.rbfor configuration
Note: Everything is fully namespaced:
- Models:
PolylingoChat::Conversation,PolylingoChat::Participant,PolylingoChat::Message - Controllers:
PolylingoChat::ConversationsController,PolylingoChat::MessagesController - Tables:
polylingo_chat_conversations,polylingo_chat_participants,polylingo_chat_messages - Routes:
/polylingo_chat/conversations,/polylingo_chat/messages
# config/application.rb or config/environments/production.rb
config.active_job.queue_adapter = :sidekiq # or :solid_queue, :delayed_job, :asyncbin/rails generate migration AddPreferredLanguageToUsers preferred_language:string
bin/rails db:migrateEdit config/initializers/polylingo_chat.rb (created by installer):
Option A: Chat-Only Mode (No Translation)
PolylingoChat.configure do |config|
# Leave api_key as nil for chat-only mode
config.api_key = nil
config.queue_adapter = :sidekiq
config.async = true
endOption B: With AI Translation
PolylingoChat.configure do |config|
# Enable translation by setting API key
config.provider = :openai # or :anthropic, :gemini
config.api_key = ENV['OPENAI_API_KEY']
config.model = 'gpt-4o-mini'
config.queue_adapter = :sidekiq
config.default_language = 'en'
config.cache_store = Rails.cache
config.timeout = 15
config.async = true
endThe installer creates app/channels/application_cable/connection.rb with authentication logic. For development, it accepts user_id from WebSocket params:
# app/channels/application_cable/connection.rb
def find_verified_user
user_id = request.params[:user_id] || cookies.encrypted[:user_id]
if user_id && (verified_user = User.find_by(id: user_id))
verified_user
else
reject_unauthorized_connection
end
endThe JavaScript consumer automatically passes window.currentUserId to authenticate. Make sure to set this in your views:
<!-- app/views/conversations/show.html.erb -->
<script>
window.conversationId = <%= @conversation.id %>;
window.currentUserId = <%= current_user.id %>;
</script>For production: Replace the find_verified_user method with your actual authentication (Devise, session, JWT, etc.)
bundle exec sidekiq # or bin/rails solid_queue:start, or bin/rails jobs:workPolylingoChat automatically detects Rails API applications and skips ActionCable/frontend setup. You can also explicitly enable API-only mode:
bin/rails generate polylingo_chat:install --api-only
bin/rails db:migrateAPI-only installation:
- ✅ Creates migrations and models
- ✅ Creates API controllers (
Api::ConversationsController,Api::MessagesController) - ✅ Adds API routes under
/apinamespace - ❌ Skips ActionCable channels
- ❌ Skips JavaScript files
- ❌ Skips Solid Cable setup
PolylingoChat supports multiple participant types out of the box. Instead of limiting conversations to just Users, you can have Vendors, Customers, Admins, or any other model as participants.
# Create a conversation
conversation = PolylingoChat::Conversation.create!(title: "Support Ticket #123")
# Add different types of participants
user = User.find(1)
vendor = Vendor.find(5)
admin = Admin.find(2)
conversation.add_participant(user, role: 'customer')
conversation.add_participant(vendor, role: 'vendor')
conversation.add_participant(admin, role: 'support')
# Get all participants regardless of type
conversation.participantables
# => [#<User id: 1>, #<Vendor id: 5>, #<Admin id: 2>]
# Get participants of a specific type
conversation.participantables_of_type(Vendor)
# => [#<Vendor id: 5>]
# Backward compatibility - still works with User model
conversation.users # Returns all User participants# Any model can send a message
vendor = Vendor.find(5)
PolylingoChat::Message.create!(
conversation: conversation,
sender: vendor, # Polymorphic - can be User, Vendor, Customer, etc.
body: "We've shipped your order!"
)
# Message model automatically handles sender_name
message.sender_name # Tries: name, full_name, email, or "Unknown"PolylingoChat provides JSON API endpoints that work for both API-only and full-stack Rails applications:
POST /polylingo_chat/conversations
Content-Type: application/json
{
"conversation": {
"title": "Project Discussion"
},
"participant_ids": [
{ "type": "User", "id": 1, "role": "owner" },
{ "type": "Vendor", "id": 5, "role": "participant" }
]
}POST /polylingo_chat/conversations/:conversation_id/messages
Content-Type: application/json
{
"message": {
"body": "Hello! How are you?",
"language": "en"
},
"sender_type": "User",
"sender_id": 1
}# Get conversation without specific language
GET /polylingo_chat/conversations/:id.json
# Get conversation with Spanish translations
GET /polylingo_chat/conversations/:id.json?lang=es
Response:
{
"id": 1,
"title": "Project Discussion",
"participants": [
{
"id": 1,
"type": "User",
"participant_id": 1,
"role": "owner",
"name": "John Doe"
},
{
"id": 2,
"type": "Vendor",
"participant_id": 5,
"role": "participant",
"name": "ACME Corp"
}
],
"messages": [
{
"id": 1,
"body": "Hello! How are you?",
"language": "en",
"translated": true,
"translation": "¡Hola! ¿Cómo estás?",
"translation_language": "es",
"available_translations": [
{"language": "es", "text": "¡Hola! ¿Cómo estás?"},
{"language": "fr", "text": "Bonjour! Comment allez-vous?"}
],
"sender_type": "User",
"sender_id": 1,
"sender_name": "John Doe",
"created_at": "2025-01-15T10:30:00Z"
}
]
}New in v0.4.0: The ?lang= parameter returns cached translations from the database with zero API calls. The available_translations array shows all cached translations for each message.
# Get messages without specific language
GET /polylingo_chat/conversations/:conversation_id/messages.json
# Get messages with French translations (from cache)
GET /polylingo_chat/conversations/:conversation_id/messages.json?lang=fr
Response:
[
{
"id": 1,
"body": "Hello! How are you?",
"language": "en",
"translated": true,
"translation": "Bonjour! Comment allez-vous?",
"translation_language": "fr",
"available_translations": [
{"language": "es", "text": "¡Hola! ¿Cómo estás?"},
{"language": "fr", "text": "Bonjour! Comment allez-vous?"}
],
"sender_type": "User",
"sender_id": 1,
"sender_name": "John Doe",
"conversation_id": 1,
"created_at": "2025-01-15T10:30:00Z",
"updated_at": "2025-01-15T10:30:05Z"
}
]First, expose the conversation and user IDs to JavaScript in your chat view:
<!-- app/views/conversations/show.html.erb -->
<script>
window.conversationId = <%= @conversation.id %>;
window.currentUserId = <%= current_user.id %>;
</script>
<div id="messages">
<!-- Messages will appear here via ActionCable -->
</div># Create a conversation
conversation = PolylingoChat::Conversation.create!(title: "Project Discussion")
# Add participants
conversation.add_participant(user1, role: 'member')
conversation.add_participant(user2, role: 'member')
# Send a message
PolylingoChat::Message.create!(
conversation: conversation,
sender: user1,
body: "Hello! How are you?"
)What happens:
- Message is saved to database
- Background job is enqueued automatically
- Without API key: Message is broadcast as-is to all participants
- With API key: Message is translated to each participant's preferred language
- All participants see the message in real-time via ActionCable
The installer creates JavaScript files that handle real-time updates automatically. The key file is app/javascript/chat.js:
// This file is automatically generated by the installer
// It connects to PolylinguoChatChannel and handles incoming messages
import consumer from "channels/consumer"
consumer.subscriptions.create({
channel: "PolylinguoChatChannel",
conversation_id: window.conversationId
}, {
received(data) {
// data.message - message text (translated if translation enabled)
// data.original - original text
// data.sender_id - sender's ID
// data.translated - boolean (true if translation was used)
console.log("Received:", data.message)
console.log("Was translated:", data.translated)
// The generated code automatically appends messages to #messages div
}
})You don't need to write this code — the installer creates it for you!
Translation is optional. Use it when:
- ✅ You have a global user base speaking different languages
- ✅ You want automatic message translation
- ✅ You're willing to pay for AI API usage
Skip translation when:
- ❌ All users speak the same language
- ❌ You want a simple chat without AI costs
- ❌ You'll handle translation elsewhere
config.provider = :openai
config.api_key = ENV['OPENAI_API_KEY']
config.model = 'gpt-4o-mini' # Fast & cost-effectiveGet key: https://platform.openai.com/api-keys
Models: gpt-4o-mini, gpt-4o, gpt-3.5-turbo
config.provider = :anthropic
config.api_key = ENV['ANTHROPIC_API_KEY']
config.model = 'claude-3-5-sonnet-20241022'Get key: https://console.anthropic.com/
Models: claude-3-5-sonnet-20241022, claude-3-5-haiku-20241022, claude-3-opus-20240229
config.provider = :gemini
config.api_key = ENV['GOOGLE_API_KEY']
config.model = 'gemini-1.5-flash'Get key: https://ai.google.dev/
Models: gemini-1.5-flash, gemini-1.5-pro
| Option | Type | Default | Description |
|---|---|---|---|
provider |
Symbol | :openai |
AI provider (:openai, :anthropic, :gemini) |
api_key |
String | nil |
API key - leave nil for chat-only mode |
model |
String | 'gpt-4o-mini' |
Model name for the provider |
queue_adapter |
Symbol | nil |
Which ActiveJob adapter you're using (informational) |
default_language |
String | 'en' |
Default target language (ISO 639-1 code) |
cache_store |
Cache | nil |
Rails cache store for caching translations |
async |
Boolean | true |
Enable async processing via ActiveJob |
timeout |
Integer | 15 |
API request timeout in seconds |
PolylingoChat now includes database-backed translation caching that dramatically reduces AI API costs and improves performance.
-
First Time Translation:
- User sends a message → AI translates it for each participant's language
- Translations are saved to
polylingo_chat_message_translationstable - One translation per language per message
-
Subsequent Retrievals:
- Page reloads, API calls → translations loaded from database
- Zero AI API calls for cached translations
- Instant response times
- ✅ Massive cost savings - Translate once, retrieve unlimited times
- ✅ Lightning fast - Database queries vs AI API calls
- ✅ Works offline - No dependency on AI service availability for viewing
- ✅ Perfect for mobile - Mobile apps get instant cached translations
- ✅ Scales effortlessly - Database caching scales with your infrastructure
# First time - translates and caches
curl -X POST http://localhost:3000/polylingo_chat/conversations/1/messages.json \
-H "Content-Type: application/json" \
-d '{"message":{"body":"Hello!"},"sender_type":"User","sender_id":1}'
# Response includes available_translations
{
"id": 15,
"body": "Hello!",
"language": "en",
"available_translations": [
{"language": "es", "text": "¡Hola!"},
{"language": "fr", "text": "Bonjour!"}
]
}
# Later retrievals - instant, no AI API calls
curl http://localhost:3000/polylingo_chat/conversations/1/messages.json?lang=es
# Returns cached Spanish translation immediately
{
"translation": "¡Hola!",
"translation_language": "es",
"available_translations": [...]
}The polylingo_chat_message_translations table stores all cached translations:
# Schema
create_table :polylingo_chat_message_translations do |t|
t.references :message, null: false
t.string :language, null: false # ISO 639-1 code (e.g., 'es', 'fr')
t.text :translated_text, null: false
t.timestamps
t.index [:message_id, :language], unique: true
end- User sends a message → saved to database
Message#after_create_commitenqueues translation job- Job runs in background via your ActiveJob adapter
- If API key present:
- Message translated for each participant's language
- Translations saved to
message_translationstable (cached) - Future retrievals use cached translations (zero API calls)
- If no API key: Original message used for all participants
- Messages broadcast via ActionCable (using Solid Cable)
- Recipients see messages in real-time
All models are namespaced under PolylingoChat:: and use the polylingo_chat_ table prefix to prevent conflicts:
PolylingoChat::Conversation - Chat conversation with many participants and messages
- Table:
polylingo_chat_conversations - Location:
app/models/polylingo_chat/conversation.rb participantables- Returns all participant records (polymorphic)participantables_of_type(klass)- Returns participants of a specific typeusers- Returns User participants (backward compatibility)add_participant(record, role: nil)- Adds any model as a participantincludes_participant?(record)- Checks if a record is a participant
PolylingoChat::Participant - Join table with polymorphic associations
- Table:
polylingo_chat_participants - Location:
app/models/polylingo_chat/participant.rb participantable- The actual participant (User, Vendor, Customer, etc.)- Supports multiple model types via polymorphic association
useralias for backward compatibility
PolylingoChat::Message - Individual chat message with translation tracking
- Table:
polylingo_chat_messages - Location:
app/models/polylingo_chat/message.rb sender- Polymorphic association (User, Vendor, Customer, etc.)sender_name- Helper method that works with any sender typetranslations- Has many MessageTranslation records (cached translations)translation_for(language)- Returns cached translation or original body- Automatic translation via background job if API key present
PolylingoChat::MessageTranslation - Cached translations (New in v0.4.0)
- Table:
polylingo_chat_message_translations - Location:
app/models/polylingo_chat/message_translation.rb - Stores one translation per language per message
- Unique index on
[message_id, language] - Dramatically reduces AI API costs by caching translations
PolylingoChat uses Solid Cable instead of Redis for ActionCable. Benefits:
- ✅ No Redis dependency - One less service to manage
- ✅ Message persistence - WebSocket messages stored in database
- ✅ Simpler deployment - Works anywhere your database works
- ✅ Better for development - No need to run Redis locally
- ✅ Cost-effective - No separate Redis hosting required
Solid Cable uses your existing database (SQLite, PostgreSQL, MySQL) to store WebSocket messages with configurable retention:
# config/cable.yml (configured automatically by installer)
development:
adapter: solid_cable
polling_interval: 0.1.seconds
message_retention: 1.dayThe installer runs bin/rails solid_cable:install automatically, which creates the necessary migrations.
bundle exec rspec
# Current status:
# 29 examples, 0 failures
# Line Coverage: 91.02%config.cache_store = Rails.cache- High-volume:
gpt-4o-mini,claude-3-5-haiku,gemini-1.5-flash - Quality:
gpt-4o,claude-3-5-sonnet,gemini-1.5-pro
Sidekiq:
# config/sidekiq.yml
:concurrency: 25
:queues:
- [polylingo_chat_translations, 10]
- [default, 5]Solid Queue:
# config/solid_queue.yml
workers:
- queues: polylingo_chat_translations
threads: 5
processes: 3- API Keys: Use environment variables, never commit
- User Scoping: Ensure users can only access their conversations
- ActionCable Auth: Secure channels with authentication
- Rate Limiting: Consider limiting message creation
- Content Filtering: Add profanity/spam filters if needed
Perfect for:
- Internal team chat
- Customer support (same language)
- Community forums
- Simple messaging features
Perfect for:
- Global marketplaces
- International teams
- Multi-language support systems
- Cross-border collaboration
Perfect for:
- Mobile app backends (iOS, Android, React Native)
- SPA frontends (React, Vue, Angular)
- Microservices architectures
- Custom WebSocket implementations
- Multi-platform applications
Perfect for:
- Marketplace chats (Buyers ↔ Sellers ↔ Support)
- Multi-tenant systems (Users ↔ Vendors ↔ Admins)
- B2B platforms (Companies ↔ Representatives)
- Healthcare apps (Patients ↔ Doctors ↔ Nurses)
PolylingoChat works seamlessly with frontend frameworks like React, Next.js, Vue, Angular, and more.
For a comprehensive guide on integrating with frontend applications, including:
- Complete API endpoint documentation
- React component examples with TypeScript
- Next.js integration patterns
- Real-time update strategies (polling, WebSocket, third-party services)
- Authentication patterns
- Translation query parameters
See the Frontend Integration Guide for complete details.
// Fetch messages with translation
const messages = await fetch(
`/polylingo_chat/conversations/1/messages?translate=true&target_language=es`,
{
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
}
}
).then(r => r.json());
// Send a message
await fetch('/polylingo_chat/conversations/1/messages', {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
message: { body: "Hello!" },
sender_type: "User",
sender_id: 1
})
});- Fork the repository
- Create your feature branch
- Write tests for your changes
- Commit and push
- Open a Pull Request
MIT License
Shoaib Malik
- Email: shoaib2109@gmail.com
- GitHub: @shoaibmalik786
- 🐛 Issues: GitHub Issues
- 📖 Documentation: GitHub Wiki
- 💬 Discussions: GitHub Discussions
Made with ❤️ for the global Rails community