How I use OpenAI to translate my Rails application into multiple languages
Guillaume Briday
5 minutes
I recently decided to add multiple languages to my time tracking application, Timecop-app.com, to expand its market reach.
The application is a monolith built with Ruby on Rails. Thanks to AI, you can easily automate the translation of all your keys in a flash, for almost no cost. I paid about 2 cents per new language.
We'll use OpenAI in this example, as it provided the best results.
Here’s how it looks on Timecop-app.com.
1. Before starting
If it’s not already the case, you’ll need to add all the translation keys required in your app instead of using hard-coded strings.
For instance:
<h1>
- Project
+ <%= Import.model_name.human %>
</h1>
<button>
- Save
+ <%= t('actions.save') %>
</button>
And create the corresponding translation files, config/locales/models/projects.en.yml
:
---
en:
activerecord:
models:
project: Project
and config/locales/actions.en.yml
:
---
en:
actions:
save: Save
There shouldn't be any visible difference in the UI, but now the title and the button will change according to the current locale.
For more details, you can refer to the official Rails documentation about i18n.
2. A bit of Rails I18n configuration
The gem i18n is already installed with Rails, but we need to create a new initializer to configure some settings.
# config/initializers/i18n.rb
Rails.application.configure do
config.i18n.default_locale = :en
config.i18n.available_locales = %i[en fr it es] # Needed to ensure we can switch to these locales.
end
Let’s update the ApplicationController
to change the current locale dynamically.
We will check multiple parameters to determine the locale to apply. If none match, we will fallback to the default locale.
You can use an around_action
callback and anonymous blocks (available in Ruby versions greater than 3.1):
# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
around_action :switch_locale
private
def switch_locale(&)
locale = params[:locale] ||
current_user.try(:preferred_locale) ||
extract_locale_from_accept_language_header.presence ||
I18n.default_locale
locale = I18n.default_locale if I18n.available_locales.exclude?(locale.to_sym)
I18n.with_locale(locale, &)
end
# Find the browser's default language
def extract_locale_from_accept_language_header
request.env['HTTP_ACCEPT_LANGUAGE'].to_s.scan(/^[a-z]{2}/).first
end
# Automatically include the locale in the query string
def default_url_options
{ locale: I18n.locale }
end
end
To determine the locale, we follow this priority order:
- Locale in params: If the locale is specified in the URL, such as
/en/pricing
or/fr/pricing
. - User preference: If the user has a preferred locale saved in their profile.
- Browser default: Based on the browser's default language.
- Fallback: Default to the default locale if none of the above match.
Additionally, during development, you can add a CSS snippet to easily identify missing translations.
Rails automatically applies the class translation_missing
to elements with missing translations:
.translation_missing {
background: rgb(239 68 68 / 1);
border-radius: 0.25rem;
padding: 0.25rem;
}
2.1 Save the preferred locale (optional)
You can add a preferred_locale
column to the users
table. Ensure that the column allows null values so that users can remain in "automatic" mode if they do not select a preferred locale.
class AddDefaultLocaleToUsers < ActiveRecord::Migration[7.1]
def change
add_column :users, :preferred_locale, :string, null: true
end
end
You can add a validator to the model to ensure the preferred_locale column contains a valid locale value:
class User < ApplicationRecord
validates :preferred_locale,
inclusion: { in: I18n.available_locales.map(&:to_s) },
allow_blank: true
end
And add the select input in your user profile form:
<%= f.select :preferred_locale,
options_for_select(
I18n.available_locales.map { |locale| [t("locales.#{locale}"), locale] },
current_user.preferred_locale
),
{ include_blank: "System Settings" } %>
If you use Devise, don't forget to add the parameter to the permitted_parameters
:
def configure_permitted_parameters
devise_parameter_sanitizer.permit(:account_update) do |user|
user.permit(
# ...
:preferred_locale,
# ...
)
end
end
See: https://github.com/heartcombo/devise/blob/main/lib/devise/parameter_sanitizer.rb
2.2 Configure your routes
Instead of using a query parameter like /time_entries?locale=fr
, you can create a cleaner URL structure such as /fr/time_entries
.
To achieve this, wrap the routes you want to translate in a scope with an optional locale parameter:
scope '/(:locale)', locale: /#{I18n.available_locales.join('|')}/ do
resources :time_entries
end
This setup allows you to include the locale in the URL path, making it more user-friendly and SEO-friendly.
3. Install the i18n-tasks gem
The gem i18n-tasks will be our sidekick here.
Add these gems in your Gemfile
:
group :development, :test do
gem 'i18n-tasks'
gem 'ruby-openai' # Required for automating translations
end
It offers many useful features for managing translations.
For example, you can normalize (i.e., sort) all your translation files:
$ bundle exec i18n-tasks normalize
You can find unused keys:
$ bundle exec i18n-tasks unused
Most importantly, it can identify missing keys, which is crucial for automatically translating them:
$ bundle exec i18n-tasks missing
4. Configure i18n-tasks
You need to create a YAML configuration file. Here’s an example of mine; you can adapt it to suit your needs:
# config/i18n-tasks.yml
base_locale: en
locales: [en, fr, it, es]
data:
read:
- config/locales/%{locale}.yml
- config/locales/**/*.%{locale}.yml # Important if you want to split your translation keys across multiple files.
yaml:
write:
# do not wrap lines at 80 characters
line_width: -1
Before adding automatic translations, make sure all your translation keys are properly set up in your YAML files.
5. Create OpenAI API Key
Open https://platform.openai.com/api-keys and create a new secret key.
Copy the newly generated secret key to your clipboard. You'll need to add credit to your account in order to use the API.
6. Run the translate-missing task
I prefer to run the normalize task before translating the missing keys to ensure that my files are properly formatted, but it's up to you.
$ bundle exec i18n-tasks normalize
$ OPENAI_API_KEY=your-secret-api-key-here bundle exec i18n-tasks translate-missing --backend=openai
You can run this command as often as needed whenever you add new English keys to your application.
And voilà!
7. Enjoy!
Additionally, thanks to Turbo, you can implement a seamless dropdown to switch locales without needing to reload the page!
module ApplicationHelper
def locale_flag(locale = I18n.locale)
{
fr: '🇫🇷',
es: '🇪🇸',
it: '🇮🇹',
en: '🇺🇸'
}[locale]
end
end
And in your view:
<div class="dropdown">
<a class="btn btn-secondary dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown">
<%= locale_flag %>
<%= I18n.locale %>
</a>
<ul class="dropdown-menu">
<% I18n.available_locales.each do |locale| %>
<li>
<%= link_to url_for(params.to_unsafe_h.merge(locale: locale)), class: 'dropdown-item' do %>
<%= locale_flag(locale) %>
<%= t("locales.#{locale}") %>
<% end %>
</li>
<% end %>
</ul>
</div>
8. Bonus
You can also explore these two useful projects: rails-i18n and devise-i18n.
Thank you!