i18n-tasks icon indicating copy to clipboard operation
i18n-tasks copied to clipboard

Model Custom Validators

Open jhovad opened this issue 6 years ago • 4 comments

I would like to use "standard" approach in custom validators of my model ... like that:

errors.add(:base, :my_custom_error_message)

So it is automatically placed into the scope: en.activerecord.errors.models.my_model.attributes.base.my_custom_error_message

Can I somehow config the i18n-tasks to handle this translations?

jhovad avatar Nov 27 '18 09:11 jhovad

Would love this feature too!! 👍

kieranklaassen avatar Dec 05 '18 20:12 kieranklaassen

Hello,

I was trying to find something for that as well, without success. So, I ended up by writing a custom scanner for that. I am not at all a rails nor ruby expert, so use it wisely and every update on this code is more than welcome. For now, it just fit my need.

There is a big regex matcher to find anything like (because I needed it to harmonize code...): errors.add :unit_price, :should_be_something errors.add(:unit_price, :should_be_something) errors.add(:unit_price, :should_be_something, range: range) errors.add(:unit_price, I18n.t("activerecord.errors.line_item.attributes.unit_price.should_be_something) errors.add(:unit_price, "Hard-coded error message")

# ./lib/scan_active_record_errors.rb
require 'i18n/tasks/scanners/file_scanner'
class ScanActiveRecordsErrors < I18n::Tasks::Scanners::FileScanner
  include I18n::Tasks::Scanners::OccurrenceFromPosition

  # @return [Array<[absolute key, Results::Occurrence]>]
  def scan_file(path)
    text = read_file(path)
    text.scan(/^\s*errors.add(?:\():([\w]*), (?:I18n.t\(){0,1}(?::){0,1}(?:"){0,1}([^"\r\n\),]*)/).map do |type, message|
      if type == "base"
        attribute_or_message = "messages"
      else
        attribute_or_message = "attributes.#{type}"
      end
      occurrence = occurrence_from_position(
          path, text, Regexp.last_match.offset(0).first)
      model = File.basename(path, ".rb") #.split('/').last
      # p "================"
      # p type
      # p message
      # p ["activerecord.errors.models.%s.%s.%s" % [model, attribute_or_message, message], occurrence]
      # p "================"
      ["activerecord.errors.models.%s.%s.%s" % [model, attribute_or_message, message], occurrence]
    end
  end
end

I18n::Tasks.add_scanner 'ScanActiveRecordsErrors'

and add it to your i18n-tasks.yml

# ./config/i18n-tasks.yml
<% require './lib/scan_active_record_errors.rb' %>

weber-s avatar Dec 27 '19 16:12 weber-s

Would anyone have the skill to write a similar scanner for extracting form labels for resources? The documentation for ActionView::Helpers::FormHelper states:

The text of label will default to the attribute name unless a translation is found in the current I18n locale (through helpers.label.<modelname>.<attribute>) or you specify it explicitly.

So you should be able to create a translation for a title label on a post resource like this:

app/views/posts/new.html.erb

<% form_for @post do |f| %>
  <%= f.label :title %>
  <%= f.text_field :title %>
  <%= f.submit %>
<% end %>

config/locales/en.yml

en:
  helpers:
    label:
      post:
        title: 'Customized title'

or config/locales/en.yml

en:
  activerecord:
    attributes:
      post:
        title: 'Customized title'

It would be nice to be able to extract anything of the type .label :[attribute] or .label(:[model], :[attribute]) to either one of the above solutions, whether using helpers.label.[model].[attribute] or activerecord.attributes.[model].[attribute].

This may be a bit complex, because it means succeeding in identifying the [model] in all cases, and perhaps skipping cases where the label text is explicitly defined in a hard-coded string or with an I18n.t method (seeing this will already be dealt with by i18n-tasks add-missing).

There are many different possible scenarios:

activerecord:
  attributes:
    post:
      cost: "Total cost"

label(:post, :cost)
# => <label for="post_cost">Total cost</label>

label(:post, :title, "A short title")
# => <label for="post_title">A short title</label>

label(:post, :title, "A short title", class: "title_label")
# => <label for="post_title" class="title_label">A short title</label>

label(:post, :privacy, "Public Post", value: "public")
# => <label for="post_privacy_public">Public Post</label>

label(:post, :cost) do |translation|
  content_tag(:span, translation, class: "cost_label")
end
# => <label for="post_cost"><span class="cost_label">Total cost</span></label>

label(:post, :cost) do |builder|
  content_tag(:span, builder.translation, class: "cost_label")
end
# => <label for="post_cost"><span class="cost_label">Total cost</span></label>

label(:post, :terms) do
  raw('Accept <a href="/terms">Terms</a>.')
end
# => <label for="post_terms">Accept <a href="/terms">Terms</a>.</label>

JohnRDOrazio avatar Jul 15 '21 10:07 JohnRDOrazio

Well I've actually come up with a solution which does not handle all use cases, it does however handle the basic use case for what's generated from the scaffolding generator, basically forms and labels of this type: <%= form.label :username %>:

require 'i18n/tasks/scanners/file_scanner'
class ScanResourceFormLabels < I18n::Tasks::Scanners::FileScanner
  include I18n::Tasks::Scanners::OccurrenceFromPosition

  # @return [Array<[absolute key, Results::Occurrence]>]
  def scan_file(path)
    text = read_file(path)
    text.scan(/^\s*<%= f(?:orm){0,1}.label :(.*) %>$/).map do |attribute|
      occurrence = occurrence_from_position(
          path, text, Regexp.last_match.offset(0).first)
      model = File.dirname(path).split('/').last
      # p "================"
      # p model
      # p attribute
      # p ["activerecord.attributes.%s.%s" % [model.singularize, attribute.first], occurrence]
      # p "================"
      ["activerecord.attributes.%s.%s" % [model.singularize, attribute.first], occurrence]
    end
  end
end

I18n::Tasks.add_scanner 'ScanResourceFormLabels'

JohnRDOrazio avatar Jul 15 '21 11:07 JohnRDOrazio