Overloading ActiveSupport's default to_sentence behaviour - ruby-on-rails

ActiveSupport offers the nice method to_sentence. Thus,
require 'active_support'
[1,2,3].to_sentence # gives "1, 2, and 3"
[1,2,3].to_sentence(:last_word_connector => ' and ') # gives "1, 2 and 3"
it's good that you can change the last word connector, because I prefer not to have the extra comma. but it takes so much extra text: 44 characters instead of 11!
the question: what's the most ruby-like way to change the default value of :last_word_connector to ' and '?

Well, it's localizable so you could just specify a default 'en' value of ' and ' for support.array.last_word_connector
See:
from: conversion.rb
def to_sentence(options = {})
...
default_last_word_connector = I18n.translate(:'support.array.last_word_connector', :locale => options[:locale])
...
end
Step by step guide:
First, Create a rails project
rails i18n
Next, edit your en.yml file: vim config/locales/en.yml
en:
support:
array:
last_word_connector: " and "
Finally, it works:
Loading development environment (Rails 2.3.3)
>> [1,2,3].to_sentence
=> "1, 2 and 3"

As an answer to how to override a method in general, a post here gives a nice way of doing it. It doesn't suffer from the same problems as the alias technique, as there isn't a leftover "old" method.
Here how you could use that technique with your original problem (tested with ruby 1.9)
class Array
old_to_sentence = instance_method(:to_sentence)
define_method(:to_sentence) { |options = {}|
options[:last_word_connector] ||= " and "
old_to_sentence.bind(self).call(options)
}
end
You might also want read up on UnboundMethod if the above code is confusing. Note that old_to_sentence goes out of scope after the end statement, so it isn't a problem for future uses of Array.

class Array
alias_method :old_to_sentence, :to_sentence
def to_sentence(args={})
a = {:last_word_connector => ' and '}
a.update(args) if args
old_to_sentence(a)
end
end

Related

How do I change the separator in parameterize for Rails

This seems like a dumb question, but how do I use parameterize in Rails? I've seen this doc: http://api.rubyonrails.org/classes/ActiveSupport/Inflector.html#method-i-parameterize
In my Model I can get string.parameterize to work, but I don't understand how to use the separator param. parameterize(string, separator: '') says I can't use parameterize on main, and string.parameterize(separator: '') says can't implicitly convert from Hash to String
string.parameterize without specifying any character will give you your string separated by words, removing any character that's not a letter, and "joining" them with '-':
string = 'Donald E. Knuth'
string.parameterize
# => "donald-e-knuth"
This way specifying a separator:
string.parameterize(separator: '*')
# => donald*e*knuth
The method acts using the I18n.transliterate method to the passed string, and then applying a destructive gsub! which will check any non-letter character and apply the substitution, is like to do:
# without separator specified
I18n.transliterate(string).gsub!(/[^a-z0-9\-_]+/i, 'separator')
# => Donald-E-Knuth
So this way if there's no separator specified, the method has one already defined as its third parameter:
def parameterize(string, sep = :unused, separator: '-', preserve_case: false)
...
end
Note the use is first the string and then the parameterize method call, unlike what the documentation exemplifies.
Note: Tested on ruby 2.3.1 and Rails 5.0.2 it works well, Rails 4.2.5, 4.2.6 (as you say) and 4.2.7 throws this error:
Loading development environment (Rails 4.2.5)
> string = 'x y z'
# => "x y z"
> string.parameterize(separator: '*')
TypeError: no implicit conversion of Hash into String
In Rails < 5 it must be used as ActiveSupport::Inflector.parameterize(string, separator):
> #item = Item.first
# => #<Item id: 1, name: "new item" ...>
> ActiveSupport::Inflector.parameterize(#item.name, '*')
# => "new*item"
Take a look at this for more info:
parameterize("Donald E. Knuth", separator: '_') # => "donald_e_knuth"
parameterize("^très|Jolie__ ", separator: '_') # => "tres_jolie"
This method comes from the ActiveSupport::Inflector class, so you can do:
ActiveSupport::Inflector.parameterize("Hello World Yeah!", separator: "*")
# => "Hello*World*Yeah!"

List Rails I18n strings with full keys

I'd like to be able to generate a complete list of all the I18n keys and values for a locale including the full keys. In other words if I have these files:
config/locales/en.yml
en:
greeting:
polite: "Good evening"
informal: "What's up?"
config/locales/second.en.yml
en:
farewell:
polite: "Goodbye"
informal: "Later"
I want the following output:
greeting.polite: "Good evening"
greeting.informal: "What's up?"
farewell.polite: "Goodbye"
farewell.informal: "Later"
How do I do this?
Once loaded into memory it's just a big Hash, which you can format any way you want. to access it you can do this:
I18n.backend.send(:translations)[:en]
To get a list of available translations (created by you or maybe by plugins and gems)
I18n.available_locales
Nick Gorbikoff's answer was a start but did not emit the output I wanted as described in the question. I ended up writing my own script get_translations to do it, below.
#!/usr/bin/env ruby
require 'pp'
require './config/environment.rb'
def print_translations(prefix, x)
if x.is_a? Hash
if (not prefix.empty?)
prefix += "."
end
x.each {|key, value|
print_translations(prefix + key.to_s, value)
}
else
print prefix + ": "
PP.singleline_pp x
puts ""
end
end
I18n.translate(:foo)
translations_hash = I18n.backend.send(:translations)
print_translations("", translations_hash)
Here's a working version of a method you can use to achieve your desired output
def print_tr(data,prefix="")
if data.kind_of?(Hash)
data.each do |key,value|
print_tr(value, prefix.empty? ? key : "#{prefix}.#{key}")
end
else
puts "#{prefix}: #{data}"
end
end
Usage:
$ data = YAML.load_file('config/locales/second.en.yml')
$ print_tr(data)
=>
en.farewell.polite: "Goodbye"
en.farewell.informal: "Later"

Shortcut for showing list of hashes "nicely"

When I have a list of hashes, like the result of an .attributes call, what is a short way to create a line-by-line nicely readable output?
Like a shortcut for
u.attributes.each {|p| puts p[0].to_s + ": " + p[1].to_s}
I'm not sure you can make it much shorter unless you create your own method.
A minor enhancement would be:
u.attributes.each {|k,v| puts "#{k}: #{v}"}
Or you can create an extension to Hash:
class Hash
def nice_print
each {|k,v| puts "#{k}: #{v}"}
end
end
u.attributes.nice_print
AS said in my comments, I like to use y hash or puts YAML.dump(hash) that shows your hash in yaml. It can be used for other objects too.
h = {:a => 1, :b => 2, :c => 3}
# => {:a=>1, :b=>2, :c=>3}
y h
#---
#:a: 1
#:b: 2
#:c: 3
# => nil
There is also an informative answer about it.
If you are looking for an output for development purposes (in Rails log files for instance), inspect or pretty_inspect should do it :
u.attributes.inspect
or
u.attributes.pretty_inspect
But if what you are looking for is a way to print nicely in Rails console, I believe you will have to write your own method, or use a gem like awesome_print, see : Ruby on Rails: pretty print for variable.hash_set.inspect ... is there a way to pretty print .inpsect in the console?
awesome_print is the way to go
gem install awesome_print
require "ap"
ap u.attributes

rails i18n - How to handle case of a nil date being passed ie l(nil)

I am working with a lot of legacy data and occasionally a datetime field is nil/null. This breaks the localization. Is there a recommended way of fixing this aside from doing this:
dt = nil
l(dt) unless dt.nil?
I think there is a cleaner way to fix this. I monkey patched I18n in an initializer called relaxed_i18n.rb
This is the content of that file:
module I18n
class << self
alias_method :original_localize, :localize
def localize object, options = {}
object.present? ? original_localize(object, options) : ''
end
end
end
And this is the RSpec code I used to validate the output of this method:
require 'rails_helper'
describe 'I18n' do
it "doesn't crash and burn on nil" do
expect(I18n.localize(nil)).to eq ''
end
it 'returns a date with Dutch formatting' do
date = Date.new(2013, 5, 17)
expect(I18n.localize(date, format: '%d-%m-%Y')).to eq '17-05-2013'
end
end
To extend Larry K's answer,
The helper should include a hash to pass options to I18n.
def ldate(dt, hash = {})
dt ? l(dt, hash) : nil
end
This allows you to pass options like this:
= ldate #term.end_date, format: :short
Unfortunately, there is no built-in solution. See post.
You can define your own helper that supplies the "nil" human-readable value. Eg:
def ldate(dt)
dt ? l(dt) : t("[???]")
end
I recently updated an application that uses jankeesvw's relaxed i18n method to Ruby 3.1 and found an issue with missing arguments.
The related change in the I18n gem is this: ruby-i18n/i18n#5eeaad7.
Also this Ruby 3 change is related: https://www.ruby-lang.org/en/news/2019/12/12/separation-of-positional-and-keyword-arguments-in-ruby-3-0/
Updated the code with this:
module I18n
class << self
alias original_localize localize
def localize(object, locale: nil, format: nil, **options)
object.present? ? original_localize(object, locale: locale, format: format, **options) : ''
end
end
end
And it works again!

i18n sync of locals yaml keys

Similar question, but for java, Keeping i18n resources synced
How to keep the i18n yaml locals' keys in sync? i.e. when a key is added to en.yml, how to get those to nb.yml or ru.yml?
if I add the key my_label: "some text in english" next to my_title: "a title" I'd like to get this to my other locals I specify, as I can't do all translations and it should fall back to english in other languages
e.g en.yml
somegroup:
my_tile: "a title in english"
my_label: "some text in english"
othergroup:
...
I'd like go issue a command and get the whole key and value inject into the norwegian translation and the corresponding position, if missing. Then git diff would show all translations to do in this language.
nb.yml
somegroup:
my_tile: "En tittel på norsk"
+ my_label: "some text in english"
othergroup:
...
Are there any gems that do something like this? If you think it's a good idea, maybe I should take the time to make it myself. Other approaches?
Try the i18n_translation_spawner gem, it could be helpful.
I will check i18n_translation_spawner gem. In case that someone needs a not-so-fast but do the job, i use this script:
First we extend the Hash class in order to support deep_merge and to replace all their leaf values with some string.
require 'yaml'
class Hash
def deep_merge(hash)
target = dup
hash.keys.each do |key|
if hash[key].is_a? Hash and self[key].is_a? Hash
target[key] = target[key].deep_merge(hash[key])
next
end
target[key] = hash[key]
end
target
end
def fill_all_values value
each_key do |key|
if self[key].is_a?(String)
store(key,value)
else
self[key].fill_all_values value
end
end
end
end
Now we can use our merger of translations:
def merge_yaml_i18n_files(locale_code_A,locale_code_B,untranslated_message)
hash_A = YAML.load_file("i18n/#{locale_code_A}.yml")
hash_B = YAML.load_file("i18n/#{locale_code_B}.yml")
hash_A_ut = Marshal.load(Marshal.dump(hash_A))
hash_A_ut.fill_all_values(untranslated_message)
hash_B_ut = Marshal.load(Marshal.dump(hash_B))
hash_B_ut.fill_all_values(untranslated_message)
hash_A = hash_B_ut.deep_merge(hash_A)
hash_B = hash_A_ut.deep_merge(hash_B)
puts hash_A.to_yaml
puts hash_B.to_yaml
end
And finally, we call this method with:
merge_yaml_i18n_files('en','es','untranslated')
If we apply this function in the following i18n files:
es.yaml
test:
hello: Hola
only_es: abc
en.yaml
test:
hello: Hello
only_en: def
The result will be:
es.yaml
test:
hello: Hola
only_en: untranslated
only_es: abc
en.yaml
test:
hello: Hello
only_en: def
only_es: untranslated
You can use i18n-tasks gem for this.
It scans calls such as I18n.t('some.key') and provides reports on key usage, missing, and unused keys. It can also can pre-fill missing keys, including from Google Translate, and it can remove unused keys as well.

Resources