# RouteTranslator: translated Rails routes driven by a locale dictionary

> RouteTranslator is a Rails gem that turns route paths into several languages by reading a dictionary from the locale files, so a resource called cars becomes coches in Spanish and voitures in French. It works with Rails 7.2 and 8.x, and the fallback rules in production are the part that needs a decision.

**enriclluelles/route_translator** — Translate your rails app route to various languages without the hassle

- Repository: https://github.com/enriclluelles/route_translator
- Stars: 953 · Forks: 145
- Language: Ruby
- License: MIT
- Published: 2026-10-20 · Updated: 2026-10-20 · Language: en
- Canonical page: https://hysenlabs.com/projects/enriclluelles-route-translator

## What problem RouteTranslator solves in a Rails app

RouteTranslator is a gem that lets you manage the translations of your app routes with a simple dictionary format. The problem it addresses is narrow but real for a localised product. Rails will happily serve a French or Spanish interface, but the URL still says /cars, and for a consumer site the path is part of the product, not just the copy.

The gem was started as a fork of the translate_routes plugin by Raúl Murciano, which is worth noting because it explains both the shape of the API and why a fork existed. The project itself now works with Rails 7.2 and 8.x.

The design choice that defines the gem is where the translations live. There is no separate route manifest, no YAML file of its own, and no generated code. The dictionary is read from the ordinary locale files, under a routes key, using the same I18n mechanism the rest of the application already uses. A translator who edits es.yml for button labels is editing the same file that decides the URL segments.

That is a genuine advantage over the alternative of keeping paths and translations in two places, and it is also the constraint. The dictionary is keyed by path segment, so the shape of the route determines the shape of the key, and a route whose segment cannot be expressed as a translation key will not translate.

The audience is Rails developers building a multilingual application who want localised paths without hand-writing a second route table.

## Installing the gem and generating the initializer

Installation is a normal gem addition followed by a generator, and the README walks through it as a numbered sequence.

The gem goes into the Gemfile:

```ruby
gem 'route_translator'
```

Then dependencies are installed with bundle install, and the default initializer is generated with a Rails generator:

```sh
bundle exec rails g route_translator:install
```

After that the work happens in three places: the routes file, the locale files, and the controller.

The generator step matters because it establishes where the gem's own configuration lives. Once the initializer exists, the deployment behaviour, including the fallback rules discussed later, is a configuration decision rather than a code change scattered through the app.

The repository structure reflects a mature gem rather than a script. There is a route_translator.gemspec, a Rakefile, a Guardfile for file watching during development, an Appraisals file and a gemfiles directory for testing against multiple dependency sets, a .rubocop.yml, a test directory, a CHANGELOG.md, an UPGRADING.md and a CONTRIBUTING.md.

The licence is MIT, with the licence text in the LICENSE file at the repository root, so using the gem inside a commercial Rails application carries no unusual obligation.

## Wrapping the routes you want translated in a localized block

The core mechanism is a routing DSL method. Routes that should be translated are wrapped in a localized block, and everything else stays exactly as it was.

Given an ordinary routes file with an admin namespace and a cars resource, the translated version wraps only the public resources:

```ruby
Rails.application.routes.draw do
  namespace :admin do
    resources :cars
  end

  localized do
    resources :cars

    get 'pricing', to: 'home#pricing', as: :pricing
  end
end
```

The admin namespace in this example stays outside the block, and the README is explicit that only the routes inside a localized block are translated.

That selectivity is the point of the design. Back office paths rarely need translating, and being able to leave them untouched avoids doubling the route table for paths nobody outside the company will ever see. It also means you can localise one area of an application at a time as the need arises, rather than converting the whole route file in one risky change.

The block takes regular routing calls inside it, including resources and named routes such as the pricing example, so the syntax you already use does not change. What changes is that each segment of those paths is now looked up in the translation dictionary.

## The route dictionary lives in the locale files

Translations are added to the locale files under a routes key, one entry per path segment:

```yml
es:
  routes:
    cars: coches
    new: nuevo
    pricing: precios
fr:
  routes:
    cars: voitures
    new: nouveau
    pricing: prix
```

The dictionary is keyed by segment, not by full path. The key cars is what makes the resource path translate, and new is the second segment of the new_car route. The pricing entry covers the standalone named route.

This has a practical consequence. If two different routes share a segment, they share a translation, because there is only one key for that word. That is usually what you want for a word like new, and it is occasionally wrong when the same word means different things in different places, which then needs a different route design rather than a different dictionary.

Another consequence: the number of keys scales with the number of distinct segments, not with the number of routes. A site with twenty resources in three languages may need forty or fifty dictionary entries rather than sixty.

Because the dictionary lives in the locale files, it participates in the usual translation workflow. The keys are shared with the interface strings in the same file, which means a translator working through es.yml encounters the route segments alongside the button labels, and the risk of a route key drifting out of step with the interface copy is a little lower.

## What the translated routes look like in rails routes

The README shows the output of the routes command before and after the change, which makes the mechanism concrete.

Before, there is one entry per route, with an unprefixed path such as /cars(.:format) and /cars/new(.:format). After wrapping in the localized block, each translated route appears three times, once per locale. The French variant is prefixed with the locale segment and uses the translated segment, so the car index route is /fr/voitures(.:format) and the new route is /fr/voitures/nouveau(.:format). The Spanish equivalents are /es/coches(.:format) and /es/coches/nuevo(.:format). The English routes keep the original unprefixed paths, /cars(.:format) and /cars/new(.:format), and carry a locale parameter in the output.

Two behaviours are worth noting from that output. The English version of each route stays at the root path rather than gaining an /en prefix, which means the default locale has no prefix while the others do. And the locale is attached to each route as a parameter, shown as {:locale=>"fr"}, so the router knows which locale a request arrived in.

The route count roughly triples for each localized resource, and it quadruples for the ones with both PATCH and PUT verbs, since the example output lists PATCH and PUT separately for update. That is the visible cost of localised paths in the routing table.

## Setting the locale from the URL with around_action, not before_action

Translating the path is only half of the job. The application also has to know which locale the visitor asked for, which in this scheme is carried in the route parameter.

The README instructs you to add one line to the ApplicationController, or to the controllers that have translated content:

```ruby
around_action :set_locale_from_url
```

Then it warns about the obvious alternative. You might be tempted to use before_action instead of around_action, and the instruction is blunt about it: just don't, because that could lead to thread-related issues.

That is a specific and important constraint. A before_action runs the callback before the action, and if the locale is set there, the callback can run in a different thread from the one the action later runs in, which is exactly the setup where a thread-local locale can leak or be lost. The around_action form wraps the action, so the locale is set and the action runs within the same surrounding context.

This is the kind of detail that separates a gem that works in development from one that behaves differently under a threaded server, and it is the single most important thing to copy correctly.

The controller is also where you scope the behaviour. The instruction says to add the line in your ApplicationController or in the controllers that have translated content, so you can apply it narrowly to the parts of the app that actually serve translated routes.

## Fallback behaviour differs between development and production

What happens when a route segment has no translation is where the development and production behaviour diverge, and the README treats it as a decision you have to make.

In the development environment, I18n is configured by default to not use a fallback language. When a translation is missing, it uses the translation key last segment as fallback, which in the example means cars and new, so an untranslated path keeps the original English segment rather than coming out in another language.

In the production environment, the guidance is that you should either set config.i18n.fallbacks = false or set up translations for your routes in every language.

So the rule for production is stricter than the development default, and it has a reason behind it. If fallbacks are left on in production, a missing Spanish entry produces a path that mixes languages, and worse, produces a different URL for the same page depending on which locale the visitor arrived with. That fragments your routes, your caching and your analytics for no benefit.

The choice between the two options is about ownership. Turning fallbacks off makes the missing translation visible as a missing segment rather than a silently mangled path, while supplying translations for every language keeps paths consistent at the cost of having to maintain them.

For a team shipping localised URLs, the second option is usually the one that matches the intent, and the first is the one that catches the gaps during development.

## Release history and what a major upgrade means for a routing gem

The maintenance picture is healthy for a gem at this stage of its life. The project is not archived, the last push was on 2026-09-29, and three recent releases are listed: v16.2.0 and v16.1.0 on 2026-07-29, and v16.0.1 on 2026-05-16.

Two releases on the same day is a pattern worth noting. It suggests a maintainer shipping a fix and a follow-up shortly after, which is a good sign for responsiveness, though it also means a version bump can arrive within hours of the one before it.

The version number itself carries information. A major version of 16 for a gem that started as a fork of translate_routes means the API has been revised many times over the gem's life, and the UPGRADING.md file at the repository root exists to record those changes. For a routing gem, a major version change is not a formality: it can alter which routes are generated, how the dictionary is keyed, or how the locale parameter is attached, and any of those affect every URL your application serves.

The supporting infrastructure suggests a project with real attention to compatibility. Appraisals and the gemfiles directory let the gem be tested against several dependency sets, the CI workflow runs on Ruby, the coveralls integration tracks coverage, and there is a maintainability badge from qlty.sh alongside the build status and gem version badges.

The licence is MIT. For an application that depends on localised paths, the upgrade cost to budget is reading the UPGRADING.md entry for each major version and checking the routes output of your own application against the documented changes.

## Conclusion

Adopt RouteTranslator if your Rails app needs localised URL paths and you want the translations to live in the locale files you already maintain, and if you are on Rails 7.2 or 8.x. Skip it if you only need localised interface text, because that is what I18n already does, or if you need localised paths in a Rails version outside the supported range. Before deploying, decide what happens when a translation is missing: set config.i18n.fallbacks = false in production or supply route translations for every language, and use around_action rather than before_action to set I18n.locale from the URL.

## FAQ

### How do I add RouteTranslator to a Rails application?

Add gem 'route_translator' to the Gemfile, run bundle install, and generate the initializer with bundle exec rails g route_translator:install. Then wrap the routes you want translated in a localized block inside routes.rb.

### Where do I put the route translations in RouteTranslator?

In the ordinary locale files, under a routes key, with one entry per path segment. For example es with routes containing cars: coches and new: nuevo, and fr with routes containing cars: voitures and new: nouveau.

### Why should I use around_action instead of before_action to set the locale?

The README warns explicitly against before_action because it could lead to thread-related issues. The around_action form sets the locale and runs the action within the same surrounding context, so add around_action :set_locale_from_url to the ApplicationController or to the controllers with translated content.

### Which Rails versions does RouteTranslator support?

The project states that it currently works with Rails 7.2 and 8.x. The gem is published as route_translator and the repository holds an UPGRADING.md file with notes for moving between major versions.

### What happens when a route translation is missing?

In development, I18n is configured by default to not use a fallback language, and the missing segment falls back to the translation key last segment. In production you should either set config.i18n.fallbacks = false or provide route translations for every language.

## Sources

- [enriclluelles/route_translator on GitHub](https://github.com/enriclluelles/route_translator)
- [Issues](https://github.com/enriclluelles/route_translator/issues)
- [License: MIT](https://github.com/enriclluelles/route_translator/blob/master/LICENSE)
- [README](https://github.com/enriclluelles/route_translator/blob/master/README.md)
- [Releases](https://github.com/enriclluelles/route_translator/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/enriclluelles-route-translator
