Open-source project
glebm/i18n-tasks avatar
glebm/i18n-tasks

i18n-tasks: static analysis that finds the missing and unused Rails translation keys

Manage translation and localization with static analysis, for Ruby i18n

2,159 stars283 forksRubyMIT

At a glance

What is it?
A Ruby gem that parses your code for I18n key usage so that missing translations fail a health check instead of a production request, and dead keys can be pruned on purpose.
Who is it for?
i18n-tasks solves a problem Rails leaves open by design: the i18n gem only knows a key is missing when something asks for it at runtime, so nothing in the framework notices a string you forgot to translate. Static scanning turns that into a build failure you can wire into CI, and the copyable RSpec or Minitest template exists for exactly that.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 13 days ago.
What is it written in?
Mainly Ruby, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on September 28, 2026, and from our analysis. They are not legal advice.

Editorial analysis

The two failure modes the i18n gem leaves open

The README names the problem in two bullets, and they are the whole reason this gem exists. Missing keys only blow up at runtime. A string you never translated is perfectly valid Ruby and perfectly valid YAML, and nothing complains until a user hits the code path that asks for it. The second one runs the other way: keys no longer in use may accumulate and introduce overhead, without you knowing it. Dead translations sit in your locale files for years, inflating the bundle and hiding the fact that half of them describe screens you deleted.

Both problems are invisible to tests unless you write tests that walk every locale file, which is precisely the work this gem automates. i18n-tasks analyses code statically for key usages, such as `I18n.t('some.key')`, and from that usage set derives three actions: report keys that are missing or unused, pre-fill missing keys from a translation backend, and remove unused keys.

The static approach is what makes it cheap enough to run on every commit. It also bounds what it can do. A key built at runtime from a variable cannot be resolved by reading the source, which is a limitation the README handles in a dedicated section rather than pretending it away.

Adding the gem, copying one config file, running health

Setup is three steps and one optional fourth. The gem works with any project using the Ruby i18n gem, which is the default in Rails, so the first step is a single line in the Gemfile scoped to development:

ruby
gem 'i18n-tasks', '~> 1.1.2', group: :development

The second step copies the default configuration out of the gem itself rather than asking you to write one. That matters because the config file is where the routing rules live, and reading the shipped version is faster than assembling an equivalent:

sh
$ cp $(bundle exec i18n-tasks gem-path)/templates/config/i18n-tasks.yml config/

The third step is the health check, which is the command worth remembering:

sh
$ bundle exec i18n-tasks health

Health is broader than a missing key scan. The README lists three things it verifies: whether any keys are missing or not used, whether interpolation variables are consistent across locales, and whether all the locale files are normalized, meaning auto-formatted. That third check is the one people rarely expect, and it is also the one that generates the most churn on a first run in an older codebase, since sorting every locale file touches lines you did not intend to change.

Running `bundle exec i18n-tasks` with no arguments prints the full task list with short descriptions, which is a reasonable way to see what else exists before deciding what to run.

Machine translation backends and the data they carry

Pre-filling missing keys is where i18n-tasks crosses from static analysis into a network service. Five backends are supported: `google`, `deepl`, `yandex`, `openai` and `watsonx`. The default task sends every missing key to whichever backend is configured:

sh
$ bundle exec i18n-tasks translate-missing

# accepts backend, from and locales options
$ bundle exec i18n-tasks translate-missing --from=base es fr --backend=google

The second line is the interesting form, because it names the source locale explicitly. Without `--from`, the base locale is assumed, and picking the wrong source is the kind of mistake that produces a translation file nobody can undo later without a diff.

This is also the point in the workflow where a decision needs to be made deliberately rather than by defaults. Product strings, error messages and anything a customer reads are being sent to a third-party API, and each backend has its own configuration section in the README covering credentials. The placeholders option is the offline alternative: `add-missing` fills keys from the base value or a humanized version of the key itself, using interpolation variables such as `%{value}` and `%{human_key}`, which produces a file that is complete but obviously not finished. That is usually the right first pass in CI, with machine translation reserved for a human review pass afterwards.

Unused keys, and the dynamic key problem underneath them

The remove path has a smaller surface area and a bigger trap. Two commands are involved: `unused` lists what is dead, `remove-unused` deletes it.

sh
$ bundle exec i18n-tasks unused
$ bundle exec i18n-tasks remove-unused

The trap is that a scanner reading source text cannot see a key assembled from a variable, so a key used only through `t("category.#{category.name}")` looks unused and gets removed. The README documents this exact case and gives two ways out: set `search.strict` to false in the config, or pass `--no-strict` on the command line. With strict mode off, the scanner infers the dynamic keys instead of ignoring them, which is a heuristic and can over-report in the other direction.

Ordering is another small thing that matters in review. By default the locale files come out rewritten; passing `-k` or `--keep-order` preserves the original ordering from the source language file, and the README recommends it for `remove-unused` when you want a reviewable diff. `prune` has the same flag and a different job: it removes keys from non-base locales that are absent in the base locale, which is how you clean up after a key was renamed in one file but not the others.

mv is a small language for rewriting key trees

The `mv` task is described as a versatile task to move or delete keys matching a given pattern, and the examples in the README are worth reading as a pattern language rather than as individual commands. All nodes, whether leaves or whole subtrees, that match a pattern are merged and moved to a target.

A plain rename moves one node onto another name:

sh
$ bundle exec i18n-tasks mv user account

Moving is a rename with a different destination shape, so a top level key can become a nested one:

sh
$ bundle exec i18n-tasks mv user_alerts user.alerts

And the pattern syntax goes further, with brace alternation and capture references. Children can be lifted one level:

sh
$ bundle exec i18n-tasks mv 'alerts.{:}' '\1'

while two nodes merge into a third, and non-leaf nodes can be merged back into their parent:

sh
$ bundle exec i18n-tasks mv '{user,profile}' account
$ bundle exec i18n-tasks mv '{pages}.{a,b}' '\1'

This is the feature that makes the gem more than a linter. Refactoring a translation key space is normally a find and replace across several YAML files, and a find and replace cannot know that `user` and `profile` are the same node. Here they merge, and the locale files stay consistent as a result.

Wiring health into CI and knowing what it cannot see

The optional fourth setup step is the one that changes how a team works. The gem ships a test template for both RSpec and Minitest, and copying it means every CI run checks for missing and unused translations:

sh
# RSpec
$ cp $(bundle exec i18n-tasks gem-path)/templates/rspec/i18n_spec.rb spec/

# Minitest
$ cp $(bundle exec i18n-tasks gem-path)/templates/minitest/i18n_test.rb test/

The README also has a Features and limitations section that names the cases where static scanning reaches its edge: relative keys, plural keys, reference keys, dynamic keys, `I18n.localize`, and `t()` keyword arguments, plus a note about unexpected normalization. Those are the honest list of things to check by hand when the scanner and the running app disagree, and it is the section to read before trusting a green build on a large existing codebase.

Maintenance looks ordinary from the outside. The repository is licensed MIT, the last push was on 2026-09-23, and the three most recent releases are v1.1.2 (2025-12-06), v1.1.1 (2025-11-24) and v1.1.0 (2025-11-14), so the 1.1 line is the one the quick start pins. Documentation lives at glebm.github.io/i18n-tasks, and the README table of contents is long enough that the shipped page is the reference to keep open, not the repository landing view.

Editorial conclusion

i18n-tasks solves a problem Rails leaves open by design: the i18n gem only knows a key is missing when something asks for it at runtime, so nothing in the framework notices a string you forgot to translate. Static scanning turns that into a build failure you can wire into CI, and the copyable RSpec or Minitest template exists for exactly that. The tradeoff is that the scanner can only see keys it can resolve, which is why the README spends real space on relative keys, plural keys, reference keys and the `search.strict` switch. Start with `health`, read what it reports before you run anything destructive, and only then reach for `remove-unused`.

Frequently asked questions

What is i18n-tasks used for in a Rails application?

It analyses the code statically for key usages such as I18n.t('some.key'), then reports keys that are missing or unused, can pre-fill the missing ones from a translation backend, and can remove the unused ones. The point is to catch a missing translation before a user requests it at runtime.

How do I install i18n-tasks and run the first check?

Add gem 'i18n-tasks', '~> 1.1.2', group: :development to the Gemfile, copy the default configuration with cp $(bundle exec i18n-tasks gem-path)/templates/config/i18n-tasks.yml config/, then run bundle exec i18n-tasks health. A template test for RSpec or Minitest can be copied too, so CI fails on missing or unused keys.

Which translation backends does i18n-tasks support?

Five: google, deepl, yandex, openai and watsonx. The translate-missing task takes a --backend option along with --from and locale arguments, so you can send one set of missing keys from the base locale to a specific backend for specific locales. Each backend has its own configuration section in the README.

Why does i18n-tasks report a key as unused when it is used at runtime?

The key is probably built from a variable, for example t("category.#{category.name}"), and a scanner reading source text cannot resolve that to a literal key. Set search.strict to false in the configuration or pass --no-strict so the scanner infers dynamic keys, at the cost of a heuristic that can over-report.

Official sources

  1. glebm/i18n-tasks on GitHub
  2. License: MIT
  3. Project website
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/glebm-i18n-tasks.svg)](https://hysenlabs.com/projects/glebm-i18n-tasks)