
Security News
Follow-up and Clarification on Recent Malicious Ruby Gems Campaign
A clarification on our recent research investigating 60 malicious Ruby gems.
RouteTranslator is a gem to allow you to manage the translations of your app routes with a simple dictionary format.
It started as a fork of the awesome translate_routes plugin by Raúl Murciano.
Right now it works with Rails 6.1, 7.x, and 8.0
If you have this routes.rb
file originally:
Rails.application.routes.draw do
namespace :admin do
resources :cars
end
resources :cars
end
The output of bundle exec rails routes
would be:
admin_cars GET /admin/cars(.:format) admin/cars#index
POST /admin/cars(.:format) admin/cars#create
new_admin_car GET /admin/cars/new(.:format) admin/cars#new
edit_admin_car GET /admin/cars/:id/edit(.:format) admin/cars#edit
admin_car GET /admin/cars/:id(.:format) admin/cars#show
PUT /admin/cars/:id(.:format) admin/cars#update
DELETE /admin/cars/:id(.:format) admin/cars#destroy
cars GET /cars(.:format) cars#index
POST /cars(.:format) cars#create
new_car GET /cars/new(.:format) cars#new
edit_car GET /cars/:id/edit(.:format) cars#edit
car GET /cars/:id(.:format) cars#show
PUT /cars/:id(.:format) cars#update
DELETE /cars/:id(.:format) cars#destroy
Add the gem to your Gemfile
:
gem 'route_translator'
And execute bundle install
Generate the default initializer:
bundle exec rails g route_translator:install
Wrap the groups of routes that you want to translate inside a localized
block:
Rails.application.routes.draw do
namespace :admin do
resources :cars
end
localized do
resources :cars
get 'pricing', to: 'home#pricing', as: :pricing
end
end
And add the translations to your locale files, for example:
es:
routes:
cars: coches
new: nuevo
pricing: precios
fr:
routes:
cars: voitures
new: nouveau
pricing: prix
Your routes are translated! Here's the output of your bundle exec rails routes
now:
Prefix Verb URI Pattern Controller#Action
admin_cars GET /admin/cars(.:format) admin/cars#index
POST /admin/cars(.:format) admin/cars#create
new_admin_car GET /admin/cars/new(.:format) admin/cars#new
edit_admin_car GET /admin/cars/:id/edit(.:format) admin/cars#edit
admin_car GET /admin/cars/:id(.:format) admin/cars#show
PATCH /admin/cars/:id(.:format) admin/cars#update
PUT /admin/cars/:id(.:format) admin/cars#update
DELETE /admin/cars/:id(.:format) admin/cars#destroy
cars_fr GET /fr/voitures(.:format) cars#index {:locale=>"fr"}
cars_es GET /es/coches(.:format) cars#index {:locale=>"es"}
cars_en GET /cars(.:format) cars#index {:locale=>"en"}
POST /fr/voitures(.:format) cars#create {:locale=>"fr"}
POST /es/coches(.:format) cars#create {:locale=>"es"}
POST /cars(.:format) cars#create {:locale=>"en"}
new_car_fr GET /fr/voitures/nouveau(.:format) cars#new {:locale=>"fr"}
new_car_es GET /es/coches/nuevo(.:format) cars#new {:locale=>"es"}
new_car_en GET /cars/new(.:format) cars#new {:locale=>"en"}
edit_car_fr GET /fr/voitures/:id/edit(.:format) cars#edit {:locale=>"fr"}
edit_car_es GET /es/coches/:id/edit(.:format) cars#edit {:locale=>"es"}
edit_car_en GET /cars/:id/edit(.:format) cars#edit {:locale=>"en"}
car_fr GET /fr/voitures/:id(.:format) cars#show {:locale=>"fr"}
car_es GET /es/coches/:id(.:format) cars#show {:locale=>"es"}
car_en GET /cars/:id(.:format) cars#show {:locale=>"en"}
PATCH /fr/voitures/:id(.:format) cars#update {:locale=>"fr"}
PATCH /es/coches/:id(.:format) cars#update {:locale=>"es"}
PATCH /cars/:id(.:format) cars#update {:locale=>"en"}
PUT /fr/voitures/:id(.:format) cars#update {:locale=>"fr"}
PUT /es/coches/:id(.:format) cars#update {:locale=>"es"}
PUT /cars/:id(.:format) cars#update {:locale=>"en"}
DELETE /fr/voitures/:id(.:format) cars#destroy {:locale=>"fr"}
DELETE /es/coches/:id(.:format) cars#destroy {:locale=>"es"}
DELETE /cars/:id(.:format) cars#destroy {:locale=>"en"}
pricing_fr GET /fr/prix(.:format) home#pricing {:locale=>"fr"}
pricing_es GET /es/precios(.:format) home#pricing {:locale=>"es"}
pricing_en GET /pricing(.:format) home#pricing {:locale=>"en"}
Note that only the routes inside a localized
block are translated.
In :development environment, I18n is configured by default to not use fallback language.
When a translation is missing, it uses the translation key last segment as fallback (cars
and new
in this example).
In :production environment, you should either set config.i18n.fallbacks = false
or set up translations for your routes in every languages.
If you want to set I18n.locale
from the url parameter locale, add
the following line in your ApplicationController
or in the controllers
that have translated content:
around_action :set_locale_from_url
Note: you might be tempted to use before_action
instead of around_action
: just don't. That could lead to thread-related issues.
To change the language and reload the appropriate route while staying on the same page, use the following code snippet:
link_to url_for(locale: 'es'), hreflang: 'es', rel: 'alternate'
Although locales are stored by Rails as a symbol (:es
), when linking to a page in a different locale you need to use a string ('es'
). Otherwise, instead of a namespaced route (/es/my-route
) you will get a parameterized route (/my-route?locale=es
).
If the page contains a localized slug, the above snippet does not work and a custom implementation is needed.
More information at Generating translated URLs
You can translate a namespace route by either its name
or path
option:
Wrap the namespaces that you want to translate inside a localized
block:
Rails.application.routes.draw do
localized do
namespace :admin do
resources :cars, only: :index
end
namespace :sold_cars, path: :sold do
resources :cars, only: :index
end
end
end
And add the translations to your locale files, for example:
es:
routes:
admin: administrador
cars: coches
new: nuevo
pricing: precios
sold: vendidos
fr:
routes:
admin: administrateur
cars: voitures
new: nouveau
pricing: prix
sold: vendues
Your namespaces are translated! Here's the output of your bundle exec rails routes
now:
Prefix Verb URI Pattern Controller#Action
admin_cars_fr GET /fr/administrateur/voitures(.:format) admin/cars#index {:locale=>"fr"}
admin_cars_es GET /es/administrador/coches(.:format) admin/cars#index {:locale=>"es"}
admin_cars_en GET /admin/cars(.:format) admin/cars#index {:locale=>"en"}
sold_cars_cars_fr GET /fr/vendues/voitures(.:format) sold_cars/cars#index {:locale=>"fr"}
sold_cars_cars_es GET /es/vendidos/coches(.:format) sold_cars/cars#index {:locale=>"es"}
sold_cars_cars_en GET /sold/cars(.:format) sold_cars/cars#index {:locale=>"en"}
At the moment inflections are not supported, but you can use the following workaround:
localized do
resources :categories, path_names: { new: 'new_category' }
end
en:
routes:
category: category
new_category: new
es:
routes:
category: categoria
new_category: nueva
Prefix Verb URI Pattern Controller#Action
categories_es GET /es/categorias(.:format) categories#index {:locale=>"es"}
categories_en GET /categories(.:format) categories#index {:locale=>"en"}
POST /es/categorias(.:format) categories#create {:locale=>"es"}
POST /categories(.:format) categories#create {:locale=>"en"}
new_category_es GET /es/categorias/nueva(.:format) categories#new {:locale=>"es"}
new_category_en GET /categories/new(.:format) categories#new {:locale=>"en"}
edit_category_es GET /es/categorias/:id/edit(.:format) categories#edit {:locale=>"es"}
edit_category_en GET /categories/:id/edit(.:format) categories#edit {:locale=>"en"}
category_es GET /es/categorias/:id(.:format) categories#show {:locale=>"es"}
category_en GET /categories/:id(.:format) categories#show {:locale=>"en"}
PATCH /es/categorias/:id(.:format) categories#update {:locale=>"es"}
PATCH /categories/:id(.:format) categories#update {:locale=>"en"}
PUT /es/categorias/:id(.:format) categories#update {:locale=>"es"}
PUT /categories/:id(.:format) categories#update {:locale=>"en"}
DELETE /es/categorias/:id(.:format) categories#destroy {:locale=>"es"}
DELETE /categories/:id(.:format) categories#destroy {:locale=>"en"}
You can configure RouteTranslator via an initializer or using the different environment config files.
RouteTranslator.config do |config|
config.force_locale = true
config.locale_param_key = :my_locale
end
Option | Description | Default |
---|---|---|
available_locales | Limit the locales for which URLs should be generated for. Accepts an array of strings or symbols. When empty, translations will be generated for all I18n.available_locales | [] |
disable_fallback | Create routes only for locales that have translations. For example, if we have /examples and a translation is not provided for es , the route helper of examples_es will not be created. Useful when one uses this with a locale route constraint, so non-es routes can return a 404 on a Spanish website | false |
force_locale | Force the locale to be added to all generated route paths, even for the default locale | false |
generate_unlocalized_routes | Add translated routes without deleting original unlocalized versions. Note: Autosets force_locale to true | false |
generate_unnamed_unlocalized_routes | Add the behavior of force_locale , but with a named default route which behaves as if generate_unlocalized_routes was true . root_path will redirect to /en or /es , depending on the value of I18n.locale | false |
hide_locale | Force the locale to be hidden on generated route paths | false |
host_locales | Set I18n.locale based on request.host . Useful for apps accepting requests from more than one domain. The key is a host pattern (supports wildcards for domains, subdomains, and TLDs), and the value is the locale symbol or string to use. See below for more details and examples. | {} |
locale_param_key | The param key used to set the locale to the newly generated routes | :locale |
locale_segment_proc | The locale segment of the url will by default be locale.to_s.downcase . You can supply your own mechanism via a Proc that takes locale as an argument, e.g. ->(locale) { locale.to_s.upcase } | false |
If you have an application serving requests from more than one domain, you might want to set I18n.locale
dynamically based on which domain the request is coming from.
The host_locales
option is a hash mapping hosts to locales, with full wildcard support to allow matching multiple domains/subdomains/tlds.
Host matching is case insensitive.
When a request hits your app from a domain matching one of the wild-card matchers defined in host_locales
, the locale will be set to the specified locale.
Unless you specified the force_locale
configuration option to true
, that locale will be hidden from routes (acting like a dynamic hide_locale
option).
Here are a few examples of possible mappings:
RouteTranslator.config.host_locales =
{ # Matches:
'*.es' => :es, # TLD: ['domain.es', 'subdomain.domain.es', 'www.long.string.of.subdomains.es'] etc.
'ru.wikipedia.*' => :ru, # Subdomain: ['ru.wikipedia.org', 'ru.wikipedia.net', 'ru.wikipedia.com'] etc.
'*.subdomain.domain.*' => :ru, # Mixture: ['subdomain.domain.org', 'www.subdomain.domain.net'] etc.
'news.bbc.co.uk' => :en, # Exact match: ['news.bbc.co.uk'] only
}
In the case of a host matching more than once, the order in which the matchers are defined will be taken into account, like so:
RouteTranslator.config.host_locales = { 'russia.*' => :ru, '*.com' => :en } # 'russia.com' will have locale :ru
RouteTranslator.config.host_locales = { '*.com' => :en, 'russia.*' => :ru } # 'russia.com' will have locale :en
If host_locales
option is set, the following options will be forced:
@config.force_locale = false
@config.generate_unlocalized_routes = false
@config.generate_unnamed_unlocalized_routes = false
@config.hide_locale = true
This is to avoid odd behaviour brought about by route conflicts and because host_locales
forces and hides the host-locale dynamically.
NOTE: locale from parameters has priority over the one from hosts.
If you have routes that (partially) share names in one locale, but must be translated differently in another locale, for example:
get 'people/favourites', to: 'people/products#favourites'
get 'favourites', to: 'products#favourites'
Then it is possible to provide different translations for common parts of those routes by scoping translations by a controller's namespace:
es:
routes:
favourites: favoritos
controllers:
people/products:
favourites: fans
Routes will be translated as in:
people_products_favourites_es GET /people/products/fans(.:format) people/products#favourites {:locale=>"es"}
products_favourites_es GET /products/favoritos(.:format) products#favourites {:locale=>"es"}
It is also possible to translated resources scoped into a namespace. Example:
namespace :people do
resources :products, only: :index
end
es:
routes:
controllers:
people/products:
products: productos_favoritos
Routes will be translated as in:
people_products_es GET /people/productos_favoritos(.:format) people/products#index {:locale=>"es"}
The gem will lookup translations under controllers
scope first and then lookup translations under routes
scope.
If you need complex routing as /:country/:locale/path/to/some/pages
, you can specify the position of your locale parameter in the following way:
scope ':country/:locale' do
localized do
root to: 'content#homepage'
end
end
When using RouteTranslator with Devise, you may notice that some authentication-related flash messages (such as errors after failed logins) are shown in the wrong language. This happens because Devise builds some messages in middleware, after your controller’s actions have completed. If you use RouteTranslator’s around_action
(or legacy around_filter
) to set the locale, the locale is reset after the controller yields, and middleware like Devise will see the default locale.
This only affects you if Devise is mounted inside a localized block (for example, localized { devise_for :users }
). If you do not use localized Devise routes, you do not need to change anything.
To ensure Devise’s middleware uses the correct locale, some users suggest replacing RouteTranslator’s around_action
with a before_action
. However, this is discouraged, as it can leave your application in the wrong locale after the request and may cause subtle bugs.
A better approach is to explicitly set the locale for Devise’s failure responses. Starting from Devise version 4.9.4, you can customize how Devise determines the locale for error messages, thanks to the ability to override the i18n_locale
method in the failure app. This allows you to set the locale for Devise’s middleware without changing RouteTranslator’s recommended usage.
Here’s an example:
# config/initializers/devise.rb
Devise.setup do |config|
# ...
ActiveSupport.on_load(:devise_failure_app) do
def i18n_locale
RouteTranslator.locale_from_request(request)
end
end
end
around_action
with a before_action
.i18n_locale
method in the failure app.Testing your controllers with routes-translator is easy, just add a locale parameter as String
for your localized routes. Otherwise, an ActionController::UrlGenerationError
will raise.
describe 'GET index' do
it 'should respond with success' do
# Remember to pass the locale param as String
get :index, locale: 'fr'
expect(response).to be_success
end
end
Please read through our contributing guidelines. Included are directions for opening issues, coding standards, and notes on development.
More over, if your pull request contains patches or features, you must include relevant unit tests.
FAQs
Unknown package
We found that route_translator demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 3 open source maintainers collaborating on the project.
Did you know?
Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.
Security News
A clarification on our recent research investigating 60 malicious Ruby gems.
Security News
ESLint now supports parallel linting with a new --concurrency flag, delivering major speed gains and closing a 10-year-old feature request.
Research
/Security News
A malicious Go module posing as an SSH brute forcer exfiltrates stolen credentials to a Telegram bot controlled by a Russian-speaking threat actor.