<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">

  <title><![CDATA[Andy Croll]]></title>
  <link href="https://andycroll.com/index.xml" rel="self"/>
  <link href="https://andycroll.com/"/>
  <updated>2026-05-06T18:41:11+00:00</updated>
  <id>https://andycroll.com/</id>
  <author>
    <name><![CDATA[Andy Croll]]></name>
    <email><![CDATA[andy@goodscary.com]]></email>
  </author>
  <generator uri="http://jekyllrb.com/">Jekyll</generator>

  
  <entry>
    <title type="html"><![CDATA[Avoid html_safe with Tag Helpers, safe_join, and sanitize]]></title>
    <link href="https://andycroll.com/ruby/alternatives-to-html-safe-in-rails/"/>
    <updated>2026-05-04T09:00:00+00:00</updated>
    <id>https://andycroll.com/ruby/alternatives-to-html-safe-in-rails</id>
    <content type="html"><![CDATA[<p>When you need to build HTML outside of a template, it’s tempting to concatenate strings and call <code class="language-plaintext highlighter-rouge">html_safe</code> on the result. This bypasses Rails’s built-in <a href="/ruby/beware-of-raw-erb/">XSS protection</a> entirely: any user input in that string goes straight to the browser unescaped.</p>

<p>The good news is you almost never need <code class="language-plaintext highlighter-rouge">html_safe</code>. Rails provides three underappreciated tools that handle escaping for you.</p>

<h2 id="instead-of">Instead of…</h2>

<p>…calling <code class="language-plaintext highlighter-rouge">html_safe</code> on strings you’ve built by hand:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">status_badge</span><span class="p">(</span><span class="n">label</span><span class="p">,</span> <span class="n">color</span><span class="p">)</span>
  <span class="s2">"&lt;span class=</span><span class="se">\"</span><span class="s2">badge badge-</span><span class="si">#{</span><span class="n">color</span><span class="si">}</span><span class="se">\"</span><span class="s2">&gt;</span><span class="si">#{</span><span class="n">label</span><span class="si">}</span><span class="s2">&lt;/span&gt;"</span><span class="p">.</span><span class="nf">html_safe</span>
<span class="k">end</span>
</code></pre></div></div>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">formatted_address</span><span class="p">(</span><span class="n">user</span><span class="p">)</span>
  <span class="p">[</span><span class="n">user</span><span class="p">.</span><span class="nf">street</span><span class="p">,</span> <span class="n">user</span><span class="p">.</span><span class="nf">city</span><span class="p">,</span> <span class="n">user</span><span class="p">.</span><span class="nf">postcode</span><span class="p">].</span><span class="nf">compact</span><span class="p">.</span><span class="nf">join</span><span class="p">(</span><span class="s2">"&lt;br&gt;"</span><span class="p">).</span><span class="nf">html_safe</span>
<span class="k">end</span>
</code></pre></div></div>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">render_comment</span><span class="p">(</span><span class="n">comment</span><span class="p">)</span>
  <span class="n">comment</span><span class="p">.</span><span class="nf">body_html</span><span class="p">.</span><span class="nf">html_safe</span>
<span class="k">end</span>
</code></pre></div></div>

<h2 id="use">Use…</h2>

<p>…the right tool for each situation.</p>

<p>When you need to <strong>build HTML elements</strong>, use <code class="language-plaintext highlighter-rouge">tag</code> helpers:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">status_badge</span><span class="p">(</span><span class="n">label</span><span class="p">,</span> <span class="n">color</span><span class="p">)</span>
  <span class="n">tag</span><span class="p">.</span><span class="nf">span</span><span class="p">(</span><span class="n">label</span><span class="p">,</span> <span class="ss">class: </span><span class="s2">"badge badge-</span><span class="si">#{</span><span class="n">color</span><span class="si">}</span><span class="s2">"</span><span class="p">)</span>
<span class="k">end</span>
</code></pre></div></div>

<p>The <a href="https://api.rubyonrails.org/classes/ActionView/Helpers/TagHelper.html"><code class="language-plaintext highlighter-rouge">tag</code> helper</a> escapes the content and attributes automatically. It returns an HTML-safe string without you having to think about it.</p>

<p>When you need to <strong>join fragments</strong> that mix safe HTML with potentially unsafe text, use <code class="language-plaintext highlighter-rouge">safe_join</code>:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">formatted_address</span><span class="p">(</span><span class="n">user</span><span class="p">)</span>
  <span class="n">safe_join</span><span class="p">([</span><span class="n">user</span><span class="p">.</span><span class="nf">street</span><span class="p">,</span> <span class="n">user</span><span class="p">.</span><span class="nf">city</span><span class="p">,</span> <span class="n">user</span><span class="p">.</span><span class="nf">postcode</span><span class="p">].</span><span class="nf">compact</span><span class="p">,</span> <span class="n">tag</span><span class="p">.</span><span class="nf">br</span><span class="p">)</span>
<span class="k">end</span>
</code></pre></div></div>

<p><a href="https://api.rubyonrails.org/classes/ActionView/Helpers/OutputSafetyHelper.html#method-i-safe_join"><code class="language-plaintext highlighter-rouge">safe_join</code></a> escapes any unsafe strings in the array and returns an HTML-safe result. It’s <code class="language-plaintext highlighter-rouge">Array#join</code> with protection built in.</p>

<p>When you need to <strong>accept user-provided HTML</strong> but strip dangerous tags, use <code class="language-plaintext highlighter-rouge">sanitize</code>:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">render_comment</span><span class="p">(</span><span class="n">comment</span><span class="p">)</span>
  <span class="n">sanitize</span><span class="p">(</span><span class="n">comment</span><span class="p">.</span><span class="nf">body_html</span><span class="p">)</span>
<span class="k">end</span>
</code></pre></div></div>

<p><a href="https://api.rubyonrails.org/classes/ActionView/Helpers/SanitizeHelper.html#method-i-sanitize"><code class="language-plaintext highlighter-rouge">sanitize</code></a> keeps safe tags like <code class="language-plaintext highlighter-rouge">&lt;p&gt;</code>, <code class="language-plaintext highlighter-rouge">&lt;strong&gt;</code>, and <code class="language-plaintext highlighter-rouge">&lt;em&gt;</code> while stripping <code class="language-plaintext highlighter-rouge">&lt;script&gt;</code>, event handlers, and other XSS vectors. You can customise the allowed tags and attributes, but beware of straying past the defaults — they are battle-tested and loosening them is a <a href="https://en.wiktionary.org/wiki/footgun">footgun</a>.</p>

<h2 id="why">Why?</h2>

<p>Each of these tools lets Rails manage HTML safety for you. You describe what you want — an element, a joined list, sanitised content — and the framework handles the escaping.</p>

<p><code class="language-plaintext highlighter-rouge">html_safe</code> does the opposite. It tells Rails “trust this string, don’t escape it”. That’s a promise <em>you</em> have to keep, and it’s easy to break when the inputs change or a future developer doesn’t realise user data flows through that path. Ask your friendly security consultant or penetration testing organisation why this is a bad idea.</p>

<p>The mental model is simple. Need an HTML element? <code class="language-plaintext highlighter-rouge">tag</code>. Joining fragments? <code class="language-plaintext highlighter-rouge">safe_join</code>. Accepting rich text? <code class="language-plaintext highlighter-rouge">sanitize</code>. If none of those fit, you probably need a partial or a component, not a string.</p>

<h2 id="why-not">Why not?</h2>

<p>There are legitimate uses of <code class="language-plaintext highlighter-rouge">html_safe</code>. Some gems, like <a href="https://github.com/ddnexus/pagy"><code class="language-plaintext highlighter-rouge">pagy</code></a>, return pre-built HTML strings that are safe by construction. Calling <code class="language-plaintext highlighter-rouge">html_safe</code> on their output is fine because the gem controls the content.</p>

<p>You might also see <code class="language-plaintext highlighter-rouge">html_safe</code> on strings that are genuinely static with no user input, like <code class="language-plaintext highlighter-rouge">"&amp;nbsp;".html_safe</code>. That’s harmless, but you can include the actual Unicode character instead — <code class="language-plaintext highlighter-rouge">"\u00A0"</code> gives you a non-breaking space without needing <code class="language-plaintext highlighter-rouge">html_safe</code> at all. That is ugly as hell though, so it’s your call!</p>

<p>The key question is always: could user input end up in this string? If the answer is yes, or even <em>maybe</em>, reach for <code class="language-plaintext highlighter-rouge">tag</code>, <code class="language-plaintext highlighter-rouge">safe_join</code>, or <code class="language-plaintext highlighter-rouge">sanitize</code> instead.</p>
]]></content>
  </entry>
  
  <entry>
    <title type="html"><![CDATA[Use Rails Combined Credentials]]></title>
    <link href="https://andycroll.com/ruby/use-rails-combined-credentials/"/>
    <updated>2026-04-13T06:00:00+00:00</updated>
    <id>https://andycroll.com/ruby/use-rails-combined-credentials</id>
    <content type="html"><![CDATA[<p>To deal with secrets and credential handling most Rails apps have ended up with a hotchpotch of <code class="language-plaintext highlighter-rouge">ENV.fetch</code> calls and <code class="language-plaintext highlighter-rouge">credentials.dig</code> lookups throughout the codebase, depending on where each secret lives.</p>

<p>Rails edge — and the upcoming 8.2 — fixes this.</p>

<h2 id="instead-of">Instead of…</h2>

<p>…mixing ENV and credential lookups:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">StripeChargeService</span>
  <span class="k">def</span> <span class="nf">initialize</span>
    <span class="vi">@api_key</span> <span class="o">=</span> <span class="no">ENV</span><span class="p">.</span><span class="nf">fetch</span><span class="p">(</span><span class="s2">"STRIPE_API_KEY"</span><span class="p">)</span>
    <span class="vi">@webhook_secret</span> <span class="o">=</span> <span class="no">Rails</span><span class="p">.</span><span class="nf">application</span><span class="p">.</span><span class="nf">credentials</span><span class="p">.</span><span class="nf">dig</span><span class="p">(</span><span class="ss">:stripe</span><span class="p">,</span> <span class="ss">:webhook_secret</span><span class="p">)</span>
    <span class="vi">@price_id</span> <span class="o">=</span> <span class="no">ENV</span><span class="p">.</span><span class="nf">fetch</span><span class="p">(</span><span class="s2">"STRIPE_PRICE_ID"</span><span class="p">)</span> <span class="p">{</span> <span class="no">Rails</span><span class="p">.</span><span class="nf">application</span><span class="p">.</span><span class="nf">credentials</span><span class="p">.</span><span class="nf">dig</span><span class="p">(</span><span class="ss">:stripe</span><span class="p">,</span> <span class="ss">:price_id</span><span class="p">)</span> <span class="p">}</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<h2 id="use">Use…</h2>

<p>…the combined credentials API:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">StripeChargeService</span>
  <span class="k">def</span> <span class="nf">initialize</span>
    <span class="vi">@api_key</span> <span class="o">=</span> <span class="no">Rails</span><span class="p">.</span><span class="nf">app</span><span class="p">.</span><span class="nf">creds</span><span class="p">.</span><span class="nf">require</span><span class="p">(</span><span class="ss">:stripe</span><span class="p">,</span> <span class="ss">:api_key</span><span class="p">)</span>
    <span class="vi">@webhook_secret</span> <span class="o">=</span> <span class="no">Rails</span><span class="p">.</span><span class="nf">app</span><span class="p">.</span><span class="nf">creds</span><span class="p">.</span><span class="nf">require</span><span class="p">(</span><span class="ss">:stripe</span><span class="p">,</span> <span class="ss">:webhook_secret</span><span class="p">)</span>
    <span class="vi">@price_id</span> <span class="o">=</span> <span class="no">Rails</span><span class="p">.</span><span class="nf">app</span><span class="p">.</span><span class="nf">creds</span><span class="p">.</span><span class="nf">require</span><span class="p">(</span><span class="ss">:stripe</span><span class="p">,</span> <span class="ss">:price_id</span><span class="p">)</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">require</code> raises a <code class="language-plaintext highlighter-rouge">KeyError</code> if the key is missing from all backends. For optional values, use <code class="language-plaintext highlighter-rouge">option</code>:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="no">Rails</span><span class="p">.</span><span class="nf">app</span><span class="p">.</span><span class="nf">creds</span><span class="p">.</span><span class="nf">option</span><span class="p">(</span><span class="ss">:appsignal</span><span class="p">,</span> <span class="ss">:push_api_key</span><span class="p">,</span> <span class="ss">default: </span><span class="kp">nil</span><span class="p">)</span>
<span class="c1"># Returns nil if missing — AppSignal just won't report</span>
</code></pre></div></div>

<p>To keep production secrets separate, run <code class="language-plaintext highlighter-rouge">bin/rails credentials:edit --environment production</code>. This creates a separate encrypted file with its own key:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>config/credentials.yml.enc         ← shared (dev/test)
config/master.key                  ← decrypts the shared file

config/credentials/production.yml.enc  ← production only
config/credentials/production.key      ← decrypts production
</code></pre></div></div>

<p>If <code class="language-plaintext highlighter-rouge">production.yml.enc</code> exists, Rails uses it exclusively in production — there’s no inheritance from the shared file, so duplicate any keys you need. To decrypt in production, set <code class="language-plaintext highlighter-rouge">RAILS_MASTER_KEY</code> in your hosting provider to the contents of <code class="language-plaintext highlighter-rouge">production.key</code>.</p>

<h2 id="why">Why?</h2>

<p><code class="language-plaintext highlighter-rouge">Rails.app.creds</code> checks ENV first, then falls back to encrypted credentials. You don’t need to know or care where a value is stored.</p>

<p>Nested keys like <code class="language-plaintext highlighter-rouge">:stripe, :api_key</code> map to double-underscored ENV names (<code class="language-plaintext highlighter-rouge">STRIPE__API_KEY</code>). A single key like <code class="language-plaintext highlighter-rouge">:postmark_api_token</code> checks <code class="language-plaintext highlighter-rouge">ENV["POSTMARK_API_TOKEN"]</code>.</p>

<p>This means you can move secrets between ENV and encrypted credentials without changing application code. Deploying to a provider that injects secrets via ENV? It just works. Want to move a key into the encrypted file instead? Remove the ENV variable and add it to your credentials. Your code stays the same.</p>

<p>I’ve <a href="/ruby/wrap-your-environment-variables-in-a-settings-object/">previously recommended</a> wrapping ENV in a custom Settings object. This built-in approach is better — the same clean interface with the added fallback to encrypted credentials.</p>

<h2 id="why-not">Why not?</h2>

<p>This isn’t in a released version of Rails yet — you need Rails edge (<code class="language-plaintext highlighter-rouge">main</code>), and it’s expected in Rails 8.2. If you’re on 8.1 or older, a <a href="/ruby/wrap-your-environment-variables-in-a-settings-object/">custom Settings wrapper</a> still works well.</p>

<h3 id="other-considerations">Other Considerations</h3>

<p>You can also create <code class="language-plaintext highlighter-rouge">development.yml.enc</code> and <code class="language-plaintext highlighter-rouge">test.yml.enc</code>, but I think the shared file plus a production override is clearer — and you shouldn’t be calling real APIs in your test environment anyhow.</p>

<p>Keep separate encryption keys for each environment. You could share one, but a leaked development key shouldn’t expose production secrets.</p>

<h2 id="mea-culpa">Mea Culpa</h2>

<p>I originally published this post saying <code class="language-plaintext highlighter-rouge">Rails.app.creds</code> had shipped in Rails 8.1. It hasn’t — it’s on Rails <code class="language-plaintext highlighter-rouge">main</code> and is expected in 8.2. I’ve been running Rails edge on a couple of projects and assumed this had already been released. Apologies for the confusion, and thanks to everyone who pointed it out.</p>
]]></content>
  </entry>
  
  <entry>
    <title type="html"><![CDATA[Teach Rails Irregular Plurals with Inflections]]></title>
    <link href="https://andycroll.com/ruby/teach-rails-irregular-plurals-with-inflections/"/>
    <updated>2026-03-30T06:00:00+00:00</updated>
    <id>https://andycroll.com/ruby/teach-rails-irregular-plurals-with-inflections</id>
    <content type="html"><![CDATA[<p>English has plenty of irregular plurals. Criterion becomes criteria, not criterions. Rails handles many common ones already, but your domain might include words it doesn’t know about.</p>

<h2 id="instead-of">Instead of…</h2>

<p>…accepting Rails’s best guess at a plural:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="s2">"criterion"</span><span class="p">.</span><span class="nf">pluralize</span>  <span class="c1">#=&gt; "criterions"</span>
<span class="s2">"matrix"</span><span class="p">.</span><span class="nf">pluralize</span>     <span class="c1">#=&gt; "matrices"  # this one Rails knows!</span>
</code></pre></div></div>

<h2 id="use">Use…</h2>

<p>…<code class="language-plaintext highlighter-rouge">inflect.irregular</code> to teach Rails the correct pair:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># config/initializers/inflections.rb</span>
<span class="no">ActiveSupport</span><span class="o">::</span><span class="no">Inflector</span><span class="p">.</span><span class="nf">inflections</span><span class="p">(</span><span class="ss">:en</span><span class="p">)</span> <span class="k">do</span> <span class="o">|</span><span class="n">inflect</span><span class="o">|</span>
  <span class="n">inflect</span><span class="p">.</span><span class="nf">irregular</span> <span class="s2">"criterion"</span><span class="p">,</span> <span class="s2">"criteria"</span>
<span class="k">end</span>
</code></pre></div></div>

<p>Now:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="s2">"criterion"</span><span class="p">.</span><span class="nf">pluralize</span>  <span class="c1">#=&gt; "criteria"</span>
<span class="s2">"criteria"</span><span class="p">.</span><span class="nf">singularize</span> <span class="c1">#=&gt; "criterion"</span>
</code></pre></div></div>

<h2 id="why">Why?</h2>

<p>Give <code class="language-plaintext highlighter-rouge">irregular</code> the singular and plural forms and Rails handles both directions—<code class="language-plaintext highlighter-rouge">pluralize</code> and <code class="language-plaintext highlighter-rouge">singularize</code> both work correctly.</p>

<p>A <code class="language-plaintext highlighter-rouge">Criterion</code> model will look for a <code class="language-plaintext highlighter-rouge">criteria</code> table. <code class="language-plaintext highlighter-rouge">resources :criteria</code> will route to <code class="language-plaintext highlighter-rouge">CriteriaController</code>. Association names, fixtures, and factory names all follow suit.</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">Criterion</span> <span class="o">&lt;</span> <span class="no">ApplicationRecord</span>
  <span class="c1"># table: criteria</span>
<span class="k">end</span>

<span class="k">class</span> <span class="nc">Survey</span> <span class="o">&lt;</span> <span class="no">ApplicationRecord</span>
  <span class="n">has_many</span> <span class="ss">:criteria</span>  <span class="c1"># works as expected</span>
<span class="k">end</span>
</code></pre></div></div>

<p>You can declare as many as you need:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="no">ActiveSupport</span><span class="o">::</span><span class="no">Inflector</span><span class="p">.</span><span class="nf">inflections</span><span class="p">(</span><span class="ss">:en</span><span class="p">)</span> <span class="k">do</span> <span class="o">|</span><span class="n">inflect</span><span class="o">|</span>
  <span class="n">inflect</span><span class="p">.</span><span class="nf">irregular</span> <span class="s2">"criterion"</span><span class="p">,</span> <span class="s2">"criteria"</span>
  <span class="n">inflect</span><span class="p">.</span><span class="nf">irregular</span> <span class="s2">"goose"</span><span class="p">,</span> <span class="s2">"geese"</span>
<span class="k">end</span>
</code></pre></div></div>

<p>Although, unless you’re building some kind of flighted animal tracker, you probably won’t need that second one.</p>

<p>Rails already knows a handful of irregular plurals: person/people, man/men, child/children, sex/sexes, move/moves, and—crucially—zombie/zombies are built in. Rails’s pluralisation rules are regex-based, so the <code class="language-plaintext highlighter-rouge">(m)an → (m)en</code> pattern also covers woman/women. But that’s it—words like tooth/teeth, foot/feet, mouse/mice, and goose/geese are not handled by default. Check the <a href="https://github.com/rails/rails/blob/main/activesupport/lib/active_support/inflections.rb">default inflections</a> to see what’s already covered.</p>

<p>See the <a href="https://api.rubyonrails.org/classes/ActiveSupport/Inflector/Inflections.html#method-i-irregular">ActiveSupport::Inflector::Inflections documentation</a> for details.</p>

<h2 id="why-not">Why not?</h2>

<p>Before adding an irregular inflection, check whether Rails already knows the word. Try it in a console:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="s2">"person"</span><span class="p">.</span><span class="nf">pluralize</span>  <span class="c1">#=&gt; "people"  — already works</span>
<span class="s2">"axis"</span><span class="p">.</span><span class="nf">pluralize</span>    <span class="c1">#=&gt; "axes"    — already works</span>
</code></pre></div></div>

<p>If it’s already correct, adding it to your initialiser is just noise.</p>

<p>If the word never appears as a model or resource name, there’s no reason to declare it.</p>

<p>For words that don’t change between singular and plural (like “sheep” or “metadata”), you need <code class="language-plaintext highlighter-rouge">inflect.uncountable</code>. For casing issues with acronyms like API or CSV, look at <code class="language-plaintext highlighter-rouge">inflect.acronym</code>.</p>
]]></content>
  </entry>
  
  <entry>
    <title type="html"><![CDATA[Handle Uncountable Words in Rails Inflections]]></title>
    <link href="https://andycroll.com/ruby/handle-uncountable-words-in-rails-inflections/"/>
    <updated>2026-03-23T06:00:00+00:00</updated>
    <id>https://andycroll.com/ruby/handle-uncountable-words-in-rails-inflections</id>
    <content type="html"><![CDATA[<p>Some English words don’t have a separate plural form. “Staff” is staff, “metadata” is metadata, “feedback” is feedback. Rails doesn’t always know this—it will happily generate a <code class="language-plaintext highlighter-rouge">staffs</code> table or a <code class="language-plaintext highlighter-rouge">metadatas</code> route if you let it.</p>

<h2 id="instead-of">Instead of…</h2>

<p>…fighting Rails when it pluralises words that shouldn’t change:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="s2">"staff"</span><span class="p">.</span><span class="nf">pluralize</span>    <span class="c1">#=&gt; "staffs"</span>
<span class="s2">"metadata"</span><span class="p">.</span><span class="nf">pluralize</span> <span class="c1">#=&gt; "metadatas"</span>
<span class="s2">"feedback"</span><span class="p">.</span><span class="nf">pluralize</span> <span class="c1">#=&gt; "feedbacks"</span>
</code></pre></div></div>

<h2 id="use">Use…</h2>

<p>…<code class="language-plaintext highlighter-rouge">inflect.uncountable</code> to tell Rails these words stay the same:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># config/initializers/inflections.rb</span>
<span class="no">ActiveSupport</span><span class="o">::</span><span class="no">Inflector</span><span class="p">.</span><span class="nf">inflections</span><span class="p">(</span><span class="ss">:en</span><span class="p">)</span> <span class="k">do</span> <span class="o">|</span><span class="n">inflect</span><span class="o">|</span>
  <span class="n">inflect</span><span class="p">.</span><span class="nf">uncountable</span> <span class="sx">%w[staff metadata feedback]</span>
<span class="k">end</span>
</code></pre></div></div>

<p>Now:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="s2">"staff"</span><span class="p">.</span><span class="nf">pluralize</span>    <span class="c1">#=&gt; "staff"</span>
<span class="s2">"metadata"</span><span class="p">.</span><span class="nf">pluralize</span> <span class="c1">#=&gt; "metadata"</span>
<span class="s2">"staff"</span><span class="p">.</span><span class="nf">singularize</span>  <span class="c1">#=&gt; "staff"</span>
</code></pre></div></div>

<h2 id="why">Why?</h2>

<p>Table names, route helpers, association names, and autoloading all depend on correct inflection. When Rails gets it wrong, you end up with a <code class="language-plaintext highlighter-rouge">staffs</code> table or <code class="language-plaintext highlighter-rouge">metadatas_path</code> route helpers.</p>

<p>Declaring a word as uncountable fixes this everywhere at once. The <code class="language-plaintext highlighter-rouge">Staff</code> model maps to the <code class="language-plaintext highlighter-rouge">staff</code> table. <code class="language-plaintext highlighter-rouge">resources :staff</code> generates the routes you’d expect.</p>

<p>Words worth declaring uncountable: <code class="language-plaintext highlighter-rouge">staff</code>, <code class="language-plaintext highlighter-rouge">metadata</code>, <code class="language-plaintext highlighter-rouge">feedback</code>, <code class="language-plaintext highlighter-rouge">analytics</code>, <code class="language-plaintext highlighter-rouge">aircraft</code>, <code class="language-plaintext highlighter-rouge">software</code>. You only need to add ones you’re actually using as model or resource names. You can pass a single string or an array.</p>

<p>Rails already handles some common uncountable words—<code class="language-plaintext highlighter-rouge">equipment</code>, <code class="language-plaintext highlighter-rouge">information</code>, <code class="language-plaintext highlighter-rouge">rice</code>, <code class="language-plaintext highlighter-rouge">money</code>, <code class="language-plaintext highlighter-rouge">species</code>, <code class="language-plaintext highlighter-rouge">series</code>, <code class="language-plaintext highlighter-rouge">fish</code>, <code class="language-plaintext highlighter-rouge">sheep</code>, <code class="language-plaintext highlighter-rouge">jeans</code>, and <code class="language-plaintext highlighter-rouge">police</code> work out of the box. Check the <a href="https://github.com/rails/rails/blob/main/activesupport/lib/active_support/inflections.rb">default inflections</a> to see the full list before adding your own.</p>

<p>See the <a href="https://api.rubyonrails.org/classes/ActiveSupport/Inflector/Inflections.html#method-i-uncountable">ActiveSupport::Inflector::Inflections documentation</a> for details.</p>

<h2 id="why-not">Why not?</h2>

<p>Uncountable words make associations slightly less intuitive. <code class="language-plaintext highlighter-rouge">has_many :staff</code> reads naturally, but <code class="language-plaintext highlighter-rouge">Staff.all</code> returning multiple records from a <code class="language-plaintext highlighter-rouge">staff</code> table can briefly confuse developers expecting a <code class="language-plaintext highlighter-rouge">staffs</code> table.</p>

<p>If the word is domain-specific jargon your team invented, a regular plural might actually be clearer. Reserve <code class="language-plaintext highlighter-rouge">uncountable</code> for genuinely uncountable English words, not as a shortcut to avoid a table name you don’t like.</p>

<p>This only affects pluralisation. For casing issues with acronyms like API or CSV, that’s <code class="language-plaintext highlighter-rouge">inflect.acronym</code>. For words with non-standard plurals like criterion/criteria, that’s <code class="language-plaintext highlighter-rouge">inflect.irregular</code>.</p>
]]></content>
  </entry>
  
  <entry>
    <title type="html"><![CDATA[Declare Acronyms in Rails Inflections]]></title>
    <link href="https://andycroll.com/ruby/declare-acronyms-in-rails-inflections/"/>
    <updated>2026-03-16T06:00:00+00:00</updated>
    <id>https://andycroll.com/ruby/declare-acronyms-in-rails-inflections</id>
    <content type="html"><![CDATA[<p>A lot of Rails’s naming magic comes from its clever use of inflections. <code class="language-plaintext highlighter-rouge">user.rb</code> defines the <code class="language-plaintext highlighter-rouge">User</code> class, backed by the <code class="language-plaintext highlighter-rouge">users</code> table, managed by <code class="language-plaintext highlighter-rouge">UsersController</code>, accessible at the <code class="language-plaintext highlighter-rouge">/users/</code> routes.</p>

<p>Every Rails app generates <code class="language-plaintext highlighter-rouge">config/initializers/inflections.rb</code> to let you customise this behaviour. Most developers leave it empty. Then one day you namespace a controller under <code class="language-plaintext highlighter-rouge">API</code> and Rails starts generating <code class="language-plaintext highlighter-rouge">Api::UsersController</code> instead of <code class="language-plaintext highlighter-rouge">API::UsersController</code>.</p>

<h2 id="instead-of">Instead of…</h2>

<p>…accepting the wrong casing in your class names:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># app/controllers/api/users_controller.rb</span>
<span class="k">class</span> <span class="nc">Api::UsersController</span> <span class="o">&lt;</span> <span class="no">ApplicationController</span>
<span class="k">end</span>
</code></pre></div></div>

<h2 id="use">Use…</h2>

<p>…<code class="language-plaintext highlighter-rouge">inflect.acronym</code> to teach Rails the correct casing:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># config/initializers/inflections.rb</span>
<span class="no">ActiveSupport</span><span class="o">::</span><span class="no">Inflector</span><span class="p">.</span><span class="nf">inflections</span><span class="p">(</span><span class="ss">:en</span><span class="p">)</span> <span class="k">do</span> <span class="o">|</span><span class="n">inflect</span><span class="o">|</span>
  <span class="n">inflect</span><span class="p">.</span><span class="nf">acronym</span> <span class="s2">"API"</span>
<span class="k">end</span>
</code></pre></div></div>

<p>Now Rails expects <code class="language-plaintext highlighter-rouge">API::UsersController</code>. The file path stays lowercase (<code class="language-plaintext highlighter-rouge">app/controllers/api/</code>), but the class name uses the acronym:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># app/controllers/api/users_controller.rb</span>
<span class="k">class</span> <span class="nc">API::UsersController</span> <span class="o">&lt;</span> <span class="no">ApplicationController</span>
<span class="k">end</span>
</code></pre></div></div>

<h2 id="why">Why?</h2>

<p>The <code class="language-plaintext highlighter-rouge">acronym</code> method tells ActiveSupport’s inflector to preserve the casing you specify. It affects <code class="language-plaintext highlighter-rouge">camelize</code>, <code class="language-plaintext highlighter-rouge">underscore</code>, <code class="language-plaintext highlighter-rouge">classify</code>, and <code class="language-plaintext highlighter-rouge">titleize</code>—which means it also affects autoloading and URL helpers.</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="s2">"api"</span><span class="p">.</span><span class="nf">camelize</span>        <span class="c1">#=&gt; "API"</span>
<span class="s2">"API"</span><span class="p">.</span><span class="nf">underscore</span>      <span class="c1">#=&gt; "api"</span>
<span class="s2">"api/users"</span><span class="p">.</span><span class="nf">camelize</span>  <span class="c1">#=&gt; "API::Users"</span>
</code></pre></div></div>

<p>Without the acronym declaration, you get <code class="language-plaintext highlighter-rouge">Api</code> instead:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="s2">"api"</span><span class="p">.</span><span class="nf">camelize</span>  <span class="c1">#=&gt; "Api"</span>
</code></pre></div></div>

<p>Unlike irregular plurals and uncountable words, Rails ships with <a href="https://github.com/rails/rails/blob/main/activesupport/lib/active_support/inflections.rb">no built-in acronyms</a>—every one you need, you have to declare yourself. Common ones worth adding: <code class="language-plaintext highlighter-rouge">API</code>, <code class="language-plaintext highlighter-rouge">SMS</code>, <code class="language-plaintext highlighter-rouge">CSV</code>, <code class="language-plaintext highlighter-rouge">HTML</code>, <code class="language-plaintext highlighter-rouge">PDF</code>. You need one call per term:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="no">ActiveSupport</span><span class="o">::</span><span class="no">Inflector</span><span class="p">.</span><span class="nf">inflections</span><span class="p">(</span><span class="ss">:en</span><span class="p">)</span> <span class="k">do</span> <span class="o">|</span><span class="n">inflect</span><span class="o">|</span>
  <span class="n">inflect</span><span class="p">.</span><span class="nf">acronym</span> <span class="s2">"API"</span>
  <span class="n">inflect</span><span class="p">.</span><span class="nf">acronym</span> <span class="s2">"SMS"</span>
  <span class="n">inflect</span><span class="p">.</span><span class="nf">acronym</span> <span class="s2">"CSV"</span>
<span class="k">end</span>
</code></pre></div></div>

<p>This also works for mixed-case words like <code class="language-plaintext highlighter-rouge">GraphQL</code> or <code class="language-plaintext highlighter-rouge">GitHub</code>. <code class="language-plaintext highlighter-rouge">inflect.acronym "GraphQL"</code> ensures <code class="language-plaintext highlighter-rouge">"graphql".camelize</code> returns <code class="language-plaintext highlighter-rouge">"GraphQL"</code> rather than <code class="language-plaintext highlighter-rouge">"Graphql"</code>.</p>

<p>See the <a href="https://api.rubyonrails.org/classes/ActiveSupport/Inflector/Inflections.html#method-i-acronym">ActiveSupport::Inflector::Inflections documentation</a> for details.</p>

<p>Note that because these changes are in an initializer, you’ll need to restart your Rails server after making changes.</p>

<h2 id="why-not">Why not?</h2>

<p>Keep the list short. Every entry changes how <code class="language-plaintext highlighter-rouge">camelize</code>, <code class="language-plaintext highlighter-rouge">titleize</code>, <code class="language-plaintext highlighter-rouge">humanize</code>, and <code class="language-plaintext highlighter-rouge">underscore</code> behave for the specified words across your entire app. Only add acronyms you’re actively using—whether in class names, attribute labels, or view helpers.</p>

<p>This only affects casing, not pluralisation. For words with non-standard plurals like criterion/criteria, you’ll want <code class="language-plaintext highlighter-rouge">inflect.irregular</code>. For words that don’t pluralise at all, the method to look at is <code class="language-plaintext highlighter-rouge">inflect.uncountable</code>.</p>
]]></content>
  </entry>
  
</feed>
