I18n for Authorization Messages

Hard-coding error messages in your controller means rewriting them in every app and every language. Action Policy has built-in I18n support so your authorization messages live in locale files — and translate automatically.

The Default Message

Right now the controller falls back to a generic string:

rescue_from ActionPolicy::Unauthorized do |ex|
message = ex.result.message || "Access denied."
redirect_to products_path, alert: message
end

When no locale translations exist yet, result.message returns nil and the fallback fires. Let’s fix that.

Creating the Locale File

Open . It already has a skeleton:

en:
action_policy:
policy:
product:
# Add your policy-specific translations here

Action Policy looks up messages using the key pattern:

action_policy.policy.<identifier>.<rule>

The identifier is the policy’s snake_case name without the Policy suffix — so ProductPolicy becomes product. The rule is the predicate method name: create?, update?, destroy?, and so on.

Add translations for the three rules our policy enforces:

en:
action_policy:
policy:
product:
update?: "You are not allowed to edit this product"
destroy?: "Only administrators can delete products"
create?: "You must be logged in to create products"
manage?: "You are not authorized to perform this action"
unauthorized: "Access denied"

Translation Lookup Order

When result.message is called, Action Policy walks through a chain of keys from most specific to most general:

  1. Policy-specific ruleaction_policy.policy.product.update?
  2. Ancestor policy ruleaction_policy.policy.application.update?
  3. Global ruleaction_policy.manage?
  4. Top-level defaultaction_policy.unauthorized

This means you can define a catch-all message at action_policy.unauthorized and override it only where you need something more specific — without touching any Ruby code.

result.message vs result.reasons.full_messages

These two APIs serve different purposes:

# The message for the top-level result (the rule that was applied directly)
ex.result.message
#=> "You are not allowed to edit this product"
# Messages from nested policy calls (when a policy calls allowed_to? on another policy)
ex.result.reasons.full_messages
#=> [] # empty for single-policy denials

result.message gives you the single localized message for the rule that was directly applied — this is what you want for the common case. reasons.full_messages collects messages across a chain of nested allowed_to? calls between policies; it returns an empty array when there are no nested policy checks involved.

Use result.message for single-policy authorization failures. Use reasons.full_messages when you need to surface every reason from a deep policy chain.

Interpolation with details

You can pass data from the policy into the translation string using the details hash:

app/policies/product_policy.rb
def update?
unless user.present?
details[:reason] = :not_logged_in
return deny!
end
details[:title] = record.name
deny!(:not_owner) unless user.id == record.user_id
true
end
en:
action_policy:
policy:
product:
not_owner: "You cannot edit '%{title}'"

Action Policy passes details as interpolation variables automatically. The result:

ex.result.message
#=> "You cannot edit 'Leather Wallet'"

Demo: Trigger Localized Messages

Save your locale file, then try these in the running app:

  1. Log out, then click New Product — you should see “You must be logged in to create products.”
  2. Log in as a regular user, then try to edit a product — you should see “You are not allowed to edit this product.”
  3. Log in as a regular user, then try to delete a product — you should see “Only administrators can delete products.”

You can also verify in the console:

Terminal window
$ bin/rails console
store(dev)> policy = ProductPolicy.new(Product.first, user: nil)
=> #<ProductPolicy:0x...>
store(dev)> policy.apply(:create?)
=> false
store(dev)> policy.result.message
=> "You must be logged in to create products"

Now let’s move on to testing — making sure our policies are correct and stay correct as the app evolves.

Powered by WebContainers
Files
Preparing Environment
  • Preparing Ruby runtime
  • Prepare development database
  • Starting Rails server