thinkoodle/rails-skills

i18n

Expert guidance for Rails I18n (internationalization and localization). Use when working with translations, locale files, t() / l() helpers, lazy lookups, pluralization, interpolation, date/time/number formatting, model translations, error message translations, setting locale from URL/header/session, or organizing YAML translation files. Triggers on "i18n", "internationalization", "translation", "locale", "localize", "t()", "translate", "multilingual", "pluralization", "locale file", "YAML tran…

First seen May 1, 2026

Installation

$ npx skills add thinkoodle/rails-skills --skill i18n

Summary

  • Expert guidance for Rails I18n (internationalization and localization).
  • Use when working with translations, locale files, t() / l() helpers, lazy lookups, pluralization, interpolation, date/time/number formatting, model translations, error message translations, setting locale from URL/header/session, or organizing YAML translation files.
  • Triggers on "i18n", "internationalization", "translation", "locale", "localize", "t()", "translate", "multilingual", "pluralization", "locale file", "YAML translation".

Similar popular skills

Related neighbors and high-traction skills in the same topics — useful to compare before installing.

Also in this package

Other skills from thinkoodle/rails-skills · top by installs.

npx skills add thinkoodle/rails-skills

Browse all from thinkoodle/rails-skills

More details

Agent compatibility

Declared targets from SKILL.md / docs. Unmarked agents are not listed — the skill may still install via the CLI.

Claude Code Not declared
Cursor Not declared
Codex Not declared
GitHub Copilot Not declared
Windsurf Not declared
Gemini CLI Not declared
Cline Not declared
OpenCode Not declared

Repository health

Stars 5
License MIT
Default branch master
Open issues 0
Status Active

Skill metadata

Parsed from SKILL.md frontmatter.

Allowed toolsRead, Grep, Glob, Write, Edit, Bash(bin/rails *), Bash(bundle exec rails *)

Package contents

Files included with this skill beyond the listing page.

  • skill md SKILL.md 14,612 B
  • docs SUMMARY.md 520 B

History

  1. First seen on skills.sh
  2. First recorded snapshot · 12 installs

SKILL.md

Rails I18n Expert

Internationalize and localize Rails applications using the I18n framework. Every user-facing string belongs in a locale file — never hardcode.

Philosophy

Core Principles:

  1. Every string in locale files — No hardcoded user-facing text in views, controllers, mailers, or models
  2. Lazy lookups everywhere — Use .title not books.index.title in views/controllers
  3. Organize by feature, not language — Split locale files by domain (models, views, defaults), not one giant file
  4. YAML is king — Use .yml files unless you need Ruby lambdas for date formats
  5. Fail loud in dev/test — Set raiseonmissing_translations = true so you catch missing keys early

When To Use This Skill

  • Adding I18n support to an existing Rails app
  • Creating or editing YAML locale files
  • Using t() / I18n.t() and l() / I18n.l() helpers
  • Setting up locale switching (URL, subdomain, header, user preference)
  • Translating Active Record model names, attributes, and error messages
  • Localizing dates, times, numbers, and currency
  • Setting up pluralization rules for non-English locales
  • Organizing locale files in large applications
  • Configuring fallbacks and available locales

Instructions

Step 1: Check Existing I18n Setup

Inspect the project's current I18n configuration first — mismatched conventions cause key lookup failures:

# Check existing locale files
find config/locales -name "*.yml" -o -name "*.rb" | sort

# Check I18n config
grep -r "i18n" config/application.rb config/initializers/ config/environments/

# Check available locales
grep -r "available_locales" config/

# Check for existing translation usage
rg "I18n\.t\b|\ t[\(\ ][\'\"\.]" --type ruby --type erb -l

# Check for hardcoded strings in views (potential I18n candidates)
rg -l ">[A-Z][a-z]+" app/views/ --type erb

Match existing conventions. If the project uses flat keys, don't introduce nested. If they organize by feature, follow that.

Step 2: Configure I18n Properly

Minimum viable config in config/application.rb:

# config/application.rb
config.i18n.available_locales = [:en, :es, :fr]
config.i18n.default_locale = :en
config.i18n.fallbacks = true  # Falls back to default_locale
config.i18n.load_path += Dir[Rails.root.join("config", "locales", "**", "*.{rb,yml}")]

In test/development — catch missing translations:

# config/environments/test.rb
config.i18n.raise_on_missing_translations = true

# config/environments/development.rb
config.i18n.raise_on_missing_translations = true

Set available_locales — without it, any locale string is accepted, which opens the door to file-system traversal attacks and unexpected fallback behavior.

Step 3: Use Translation Helpers Correctly

Basic Lookups

# In views (translate helper auto-available)
t("hello")                          # Simple key
t("messages.welcome")               # Nested key
t(:welcome, scope: :messages)       # Same thing, symbol + scope

# In controllers/models/services
I18n.t("messages.welcome")

# With default fallback
t("missing.key", default: "Fallback text")
t("missing.key", default: [:other_key, "Final fallback"])

Lazy Lookups (PREFER THESE)

Lazy lookups auto-scope based on the view path or controller action:

# config/locales/en.yml
en:
  books:
    index:
      title: "All Books"
      empty: "No books found"
    show:
      title: "Book Details"
    create:
      success: "Book created!"
      failure: "Could not create book."
<%# app/views/books/index.html.erb %>
<h1><%= t(".title") %></h1>         <%# Resolves to books.index.title %>
<p><%= t(".empty") %></p>           <%# Resolves to books.index.empty %>
# app/controllers/books_controller.rb
class BooksController < ApplicationController
  def create
    if @book.save
      redirect_to @book, notice: t(".success")   # books.create.success
    else
      flash.now[:alert] = t(".failure")           # books.create.failure
      render :new, status: :unprocessable_entity
    end
  end
end

Prefer lazy lookups (.key) in views and controllers — they keep translation keys DRY and tied to the file structure. Only use full paths when referencing shared/global keys.

Interpolation

en:
  greeting: "Hello, %{name}!"
  item_count: "You have %{count} items in %{location}"
t("greeting", name: current_user.name)
t("item_count", count: 5, location: "your cart")

Don't use scope or default as interpolation variable names — they're reserved by I18n and raise I18n::ReservedInterpolationKey.

Pluralization

en:
  notifications:
    zero: "No notifications"      # optional for English
    one: "1 notification"
    other: "%{count} notifications"
t("notifications", count: 0)   # => "No notifications"
t("notifications", count: 1)   # => "1 notification"
t("notifications", count: 42)  # => "42 notifications"

The :count variable is magic — it selects the plural form AND interpolates into the string.

English needs only one and other. Other languages need different forms:

  • Arabic: zero, one, two, few, many, other
  • Russian: one, few, many, other
  • Japanese: other only

Use the rails-i18n gem for locale-specific pluralization rules.

HTML-Safe Translations

Keys ending in _html or named html are automatically marked HTML-safe in views:

en:
  welcome_html: "<strong>Welcome</strong> to %{app_name}"
  help:
    html: "Need <em>help</em>? <a href='%{url}'>Contact us</a>"
<%= t("welcome_html", app_name: "MyApp") %>  <%# HTML not escaped %>

Interpolated values ARE still escaped (safe against XSS). Only use _html keys when the translation itself contains markup.

Step 4: Translate Active Record Models

Model Names and Attributes

en:
  activerecord:
    models:
      user:
        one: "User"
        other: "Users"
      admin/post: "Admin Post"    # Namespaced model
    attributes:
      user:
        email: "Email address"
        first_name: "First name"
      user/role:                   # Nested attribute
        admin: "Administrator"
User.model_name.human              # => "User"
User.model_name.human(count: 2)    # => "Users"
User.human_attribute_name(:email)  # => "Email address"

Validation Error Messages

Error messages look up in this order (first match wins):

activerecord.errors.models.MODEL.attributes.ATTRIBUTE.ERROR
activerecord.errors.models.MODEL.ERROR
activerecord.errors.messages.ERROR
errors.attributes.ATTRIBUTE.ERROR
errors.messages.ERROR
en:
  activerecord:
    errors:
      models:
        user:
          attributes:
            email:
              blank: "is required — we need this to contact you"
              taken: "is already registered"
            name:
              too_short: "must be at least %{count} characters"
          # Applies to all attributes on User:
          invalid: "has a problem"
      # Applies to all models:
      messages:
        blank: "can't be empty"

  # Global fallback for all models:
  errors:
    format: "%{attribute}: %{message}"   # Customize full_message format
    messages:
      blank: "is required"

Available interpolation variables in error messages: model, attribute, value, count.

Step 5: Localize Dates, Times, and Numbers

Date/Time Formatting

en:
  date:
    formats:
      default: "%Y-%m-%d"
      short: "%b %d"
      long: "%B %d, %Y"
  time:
    formats:
      default: "%a, %d %b %Y %H:%M:%S %z"
      short: "%d %b %H:%M"
      long: "%B %d, %Y %H:%M"
l(Date.today)                        # Default format
l(Date.today, format: :short)        # Short format
l(Time.current, format: :long)       # Long format

Use l() for dates/times — strftime ignores the current locale, so dates won't format correctly for non-English users.

Number Formatting

Number helpers (numbertocurrency, numberwithdelimiter, etc.) read from locale files:

en:
  number:
    format:
      separator: "."
      delimiter: ","
      precision: 3
    currency:
      format:
        unit: "$"
        format: "%u%n"           # $1,000.00
        separator: "."
        delimiter: ","
        precision: 2

es:
  number:
    currency:
      format:
        unit: "€"
        format: "%n %u"          # 1.000,00 €
        separator: ","
        delimiter: "."

Step 6: Set Locale Per Request

Use aroundaction with I18n.withlocale — setting I18n.locale = directly leaks across requests in threaded servers (Puma), causing users to see other users' locales.

From URL Path (Recommended)

# config/routes.rb
scope "(:locale)", locale: /en|es|fr/ do
  resources :books
  # ... all routes
end

# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
  around_action :switch_locale

  def switch_locale(&action)
    locale = params[:locale] || I18n.default_locale
    I18n.with_locale(locale, &action)
  end

  def default_url_options
    { locale: I18n.locale }
  end
end

Combined Priority Chain

def resolve_locale
  params[:locale].presence ||
    current_user&.locale.presence ||
    request.env["HTTP_ACCEPT_LANGUAGE"]&.scan(/^[a-z]{2}/)&.first&.then { |l|
      l.to_sym if I18n.available_locales.include?(l.to_sym)
    } ||
    I18n.default_locale
end

Step 7: Organize Locale Files

Small apps: one file per locale (config/locales/en.yml, es.yml).

Medium/large apps — split by concern:

config/locales/
  defaults/en.yml    # Date, time, number formats
  models/en.yml      # AR model names, attributes, errors
  views/en.yml       # View translations (lazy lookup keys)
  mailers/en.yml     # Mailer subjects and content

Load nested directories: config.i18n.load_path += Dir[Rails.root.join("config", "locales", "**", "*.{rb,yml}")]

YAML rules: Top-level key = locale. Keys are snake_case. Quote 'true'/'false'/'yes'/'no'/'on'/'off' as keys (YAML parses them as booleans otherwise). Keep nesting ≤ 4 levels.

Step 8: Action Mailer Translations

Mailer subjects auto-resolve from mailerscope.actionname.subject:

en:
  user_mailer:
    welcome:
      subject: "Welcome to %{app_name}!"
class UserMailer < ApplicationMailer
  def welcome(user)
    mail(to: user.email)  # Subject auto-resolved
    # Or with interpolation: mail(to: user.email, subject: default_i18n_subject(app_name: "MyApp"))
  end
end

Step 9: Fallbacks

# config/application.rb
config.i18n.fallbacks = true                    # Falls back to default_locale
config.i18n.fallbacks = { es: :en, fr: :en }   # Or specific chains

In development/test, keep fallbacks OFF and raiseonmissing_translations = true.

Common Agent Mistakes

  1. Hardcoding strings — "Record saved" instead of t(".success")
  2. Wrong YAML nesting — Forgetting locale key at top level, wrong indentation
  3. Not using lazy lookups — t("users.show.title") instead of t(".title") in views/controllers
  4. Forgetting available_locales — Without it, arbitrary locale strings are accepted (security risk)
  5. Using I18n.locale = — Leaks across requests in threaded servers; use I18n.with_locale
  6. Pluralization without count — Returns raw hash instead of string
  7. Missing _html suffix — HTML in translations gets escaped without it
  8. Not quoting YAML booleans — true, false, yes, no are parsed as booleans; quote them when used as keys
  9. Forgetting to restart server — New locale files require restart to load

Quick Reference

Translation Lookup Methods

Context Method Lazy Lookup
Views t(".key") or t("full.key") ✅ Yes
Controllers t(".key") or I18n.t("full.key") ✅ Yes
Models I18n.t("full.key") ❌ No
Mailers I18n.t("full.key") ❌ No
Services/Jobs I18n.t("full.key") ❌ No

Essential Locale File Structure

en:
  # View translations (lazy lookups)
  controller_name:
    action_name:
      key: "value"
  # Shared/global
  shared:
    save: "Save"
    cancel: "Cancel"
  # Active Record
  activerecord:
    models:
      model_name: { one: "Singular", other: "Plural" }
    attributes:
      model_name:
        attribute: "Label"
    errors:
      models:
        model_name:
          attributes:
            attribute:
              error_type: "message"
  # Formats (or use rails-i18n gem)
  date:
    formats: { default: "%Y-%m-%d", short: "%b %d", long: "%B %d, %Y" }
  time:
    formats: { default: "%Y-%m-%d %H:%M" }
  number:
    currency:
      format: { unit: "$" }
  # Mailers
  mailer_name:
    action_name:
      subject: "Subject line"

New Locale Checklist

  • Add to available_locales
  • Create locale files mirroring existing structure
  • Add rails-i18n gem for date/time/number defaults + pluralization rules
  • Test with raiseonmissing_translations = true
  • Add locale switcher UI + update defaulturloptions if URL-based

For detailed patterns, examples, and edge cases, see the references/ directory:

  • references/lookups.md — Translation lookup methods, interpolation, lazy lookups
  • references/locale-files.md — YAML patterns, file organization, date/time/number localization, custom backends
  • references/pluralization.md — Pluralization rules by language
  • references/model-translations.md — Active Record model/attribute/error translations
  • references/locale-switching.md — Locale switching strategies and fallback configuration
  • references/testing.md — Testing I18n, common gems (rails-i18n, i18n-tasks, mobility)