How to test that all rails translations calls pass all interpolation keys? - ruby-on-rails

I'd like to verify that all translation calls in my app pass the proper interpolation arguments. However, when no argument is passed, there is no exception raised.
Per Rails i18n documentation http://guides.rubyonrails.org/i18n.html
"If a translation expects an interpolation variable but it has not been passed to #translate an I18n::MissingInterpolationArgument exception is raised."
However, this doesnt seem to be the case if NO interpolation variables are passed. For instance, this code doesn't raise an exception:
require 'i18n'
I18n.backend.store_translations :en, :thanks => 'Thanks %{name}! '
I18n.translate :thanks # <- expect an exception, but it just returns 'Thanks %{name}!'
I18n.translate :thanks, :foo => 'bar' # <- this raises
Anyone know of a way to detect calls that are missing translation keys during the tests other than parsing for "%{" ?

This looks like a rail bug to me. Why don't you try patching it?

Related

Convert string into class name without throwing error

I am trying to convert the string into classname using constantize. But it throws error if the classname is not present or the string is empty or nil.
Is there any way to convert the string into classname without throwing error. Or returning false only if not able to convert.
I hate Rails, it brings a ton of redundant so-called helpers, having zero value in general. This might be easily done with pure Ruby:
Kernel.const_get(string) if Kernel.const_defined?(string)
The above effectively returns the class or nil (which is falsey in Ruby) if the class is not defined.
Firstly, I want to point out something from the answers above. rescue false means that all kinds of exceptions thrown from that expression are rescued as false. This means that even if you have an exception object of class NoMethodError or RuntimeError you will return false and your expectation would be that the constantized string does not match a constant in your app. This can cause you hours of debugging if you don't know how the error handling system in ruby is working. It is also a place to introduce a lot of bugs in your code in the future.
I see the ruby-on-rails tag so I assume you are running a problem in a rails app. You can use a helper method coming from the ActiveSupport::Inflector module. Instead of rescuing the constantize method you would probably want to use the safe_constantize. It will return nil in case the constant is not present in your project.
Example usage (note I haven't defined a Foo constant in my project):
# with constantize
irb(main) > 'foo'.constantize
=> NameError (wrong constant name foo)
# with safe_contantize
irb(main) > 'foo'.safe_constantize
=> nil
What's the error? if it's an exception you can do something like:
the_class = your_string.constantize rescue false
The rescue catches de exception and returns false in that case.
You can achieve it by following,
string.constantize rescue false
Additional
If you have have downcase string or table name for that, it can also be converted into class name by using classify method as follows,
string.classify.constantize rescue false
If you have string 'event_managers', it will return class EventManager
You can provide a default value or return early in case of nil and then rescue a NameError:
def to_class(name)
# default to empty string
(name || "").constantize
rescue NameError
false
end
def to_class(name)
# guard against invalid input
return false if name.nil?
name.constantize
rescue NameError
false
end
EDIT: It's longer than simple
string.constantize rescue false
but IMO you should rescue the most specific error you can. rescue false is OK for simple scripts or some test cases. In production code it's quite risky - you could hide some exceptions that should be verbose.

Ruby 2 Keyword Arguments and ActionController::Parameters

I have a rails 4 application that is running on ruby 2.1. I have a User model that looks something like
class User < ActiveModel::Base
def self.search(query: false, active: true, **extra)
# ...
end
end
As you can see in the search method I am attempting to use the new keyword arguments feature of ruby 2.
The problem is that when I call this code from in my controller all values get dumped into query.
params
{"action"=>"search", "controller"=>"users", query: "foobar" }
Please note that this is a ActionController::Parameters object and not a hash as it looks
UsersController
def search
#users = User.search(params)
end
I feel that this is because params is a ActionController::Parameters object and not a hash. However even calling to_h on params when passing it in dumps everything into query instead of the expected behavior. I think this is because the keys are now strings instead of symbols.
I know that I could build a new hash w/ symbols as the keys but this seems to be more trouble than it's worth. Ideas? Suggestions?
Keywords arguments must be passed as hash with symbols, not strings:
class Something
def initialize(one: nil)
end
end
irb(main):019:0> Something.new("one" => 1)
ArgumentError: wrong number of arguments (1 for 0)
ActionController::Parameters inherits from ActiveSupport::HashWithIndifferentAccess which defaults to string keys:
a = HashWithIndifferentAccess.new(one: 1)
=> {"one"=>1}
To make it symbols you can call symbolize_keys method. In your case: User.search(params.symbolize_keys)
I agree with Morgoth, however, with rails ~5 you will get a Deprecation Warning because ActionController::Parameters no longer inherits from hash. So instead you can do:
params.to_unsafe_hash.symbolize_keys
or if you have nested params as is often the case when building api endpoints:
params.to_unsafe_hash.deep_symbolize_keys
You might add a method to ApplicationController that looks something like this:
def unsafe_keyworded_params
#_unsafe_keyworded_params ||= params.to_unsafe_hash.deep_symbolized_keys
end
You most likely do need them to be symbols. Try this:
def search
#users = User.search(params.inject({}){|para,(k,v)| para[k.to_sym] = v; para}
end
I know it's not the ideal solution, but it is a one liner.
In this particular instance I think you're better off passing the params object and treating it as such rather than trying to be clever with the new functionality in Ruby 2.
For one thing, reading this is a lot clearer about where the variables are coming from and why they might be missing/incorrect/whatever:
def search(params)
raise ArgumentError, 'Required arguments are missing' unless params[:query].present?
# ... do stuff ...
end
What you're trying to do (in my opinion) only clouds the issue and confuses things when trying to debug problems:
def self.search(query: false, active: true, **extra)
# ...
end
# Method explicitly asks for particular arguments, but then you call it like this:
User.search(params)
Personally, I think that code is a bit smelly.
However ... personal opinion aside, how I would fix it would be to monkey-patch the ActionController::Parameters class and add a #to_h method which structured the data as you need it to pass to a method like this.
Using to_unsafe_hash is unsafe because it includes params that are not permitted. (See ActionController::Parameters#permit) A better approach is to use to_hash:
params.to_hash.symbolize_keys
or if you have nested params:
params.to_hash.deep_symbolize_keys
Reference: https://api.rubyonrails.org/classes/ActionController/Parameters.html#method-i-to_hash

Rescue from single resource exceptions in Rails index action

For the show action, returning a 500 error on uncaught exceptions is fine, but for the index action, it would be helpful if a single bad resource didn't cause the whole request to fail. Is there a way to rescue from those exceptions and return the rest of the resources along with a list of errors?
Details: I'm using RABL to render JSON templates like this (but I think the solution is likely general rather than specific to this):
# app/controllers/happenings_controller.rb
def index
#happenings = current_person.happenings
end
# app/views/happening/index.json.rabl
collection #happenings
extends 'happenings/show'
# app/views/happening/show.json.rabl
object #happening
attributes :id, :name, :description
node :creator, if: lambda { |s| s.creator? } do |s|
# !!! This is where an exception on a single resource was blowing up the whole request
partial("people/show", :object => s.creator)
end
Sure! Just rescue the exception.
So, this answer probably does not help you at all, but it is just because you did not provide any meaningful description about your problem.
It all depends on where your errors occur. Do you want to rescue from database queries, rendering errors, environment problems?
Depending on the cause of the error there are different solutions like monkey-patching rabl, a simple rescue in your controller or some middleware handling the error.

How to use i18n key as default translation in Rails 3?

For example:
I18n.t('something')
should output only
something
if the translation missing.
It is possible:
See section 4.1.2 Defaults at Rails Internationalization (I18n) API.
I18n.t :missing, :default => 'Not here'
# => 'Not here'
On rails 4 you can change the exception handler.
Add the following to config/initializers/i18n.rb:
module I18n
class MissingTranslationExceptionHandler < ExceptionHandler
def call(exception, locale, key, options)
if exception.is_a?(MissingTranslation)
key
else
super
end
end
end
end
I18n.exception_handler = I18n::MissingTranslationExceptionHandler.new
Now on you views you can just do:
<p><%= t "Not translated!" %></p>
Guide on the subject: http://guides.rubyonrails.org/i18n.html#using-different-exception-handlers
side-note:
this might help figuring out what Rails thinks the current scope is (e.g. when using ".something")
http://unixgods.org/~tilo/Rails/which_l10n_strings_is_rails_trying_to_lookup.html
this way you can better avoid having missing translations because of incorrect placing of translations strings in the L10n-file / incorrect keys
David's answer is the right solution to the question, another (more verbose) way to do it is to rescue and return the key:
def translate_nicely(key)
begin
I18n.translate!(key)
rescue
key
end
end
nope, not possible. If you use I18 you need to have a file that corresponds to the language otherwise I18n will complain.
Of course you can set the default language in your environment.rb file. Should be near the bottom and you can set this for whatever language you want but in your locales/ folder you will need to have a corresponding yml translation.

Ruby Hash.merge with specified keys only

I'm pretty sure I saw on a Rails related site something along the lines of:
def my_function(*opts)
opts.require_keys(:first, :second, :third)
end
And if one of the keys in require_keys weren't specified, or if there were keys that weren't specified, an exception was raised. I've been looking through ActiveSupport and I guess I might be looking for something like the inverse of except.
I like to try and use as much of the framework as possible compared to writing my own code, that's the reason I'm asking when I know how to make the same functionality on my own. :)
At the moment I'm doing it through the normal merge routine and making sure that I have what I need with some IFs.
I think the method you're thinking of is assert_valid_keys (documentation here) but this only raises an exception if any unexpected keys exist in the hash, not if any of the specified keys are missing. i.e. if a hash is being used to pass options to a method it can be used to check for invalid options not for required options.
You can do this yourself relatively easily. As was stated in an earlier answer, half your work is done for you in assert_valid_keys. You can roll your own method to do the rest.
def my_function( *opts )
opts.require_and_assert_keys( :first, :second, :third )
end
create lib/hash_extensions.rb with the following:
class Hash
def require_and_assert_keys( *required_keys )
assert_valid_keys( keys )
missing_keys = required_keys.inject(missing=[]) do |missing, key|
has_key?( key ) ? missing : missing.push( key )
end
raise( ArgumentError, "Missing key(s): #{missing_keys.join( ", ")}" ) unless missing_keys.empty?
end
end
finally, in config/environment.rb, add this to make it work:
require 'hash_extensions'

Resources