How can I write quoted values in en.yml? - ruby-on-rails

I'm writing a script that will add new translations to the en.yml file. However, when I'm dumping them back to the file, my strings are in the following format:
some_key: This is the value
I'm trying to make the output be:
some_key: "This is the value"
I'm writing the translations like this:
File.open(yaml_file, "w") do |f|
f.write(translations.to_yaml)
end
Where translations is the hash containing all the translations.
Is there any way of adding these quotes, besides manually parsing/rewriting the YAML file?

The plan (unquotes) scalar representation is the preferred version when the scalar type doesn't require escaping.
In your case, the String:
This is the value
doesn't need to be in quotes, thus, if you supply the following YAML:
key: "This is the value"
the processor may return:
key: This is the value
because they are totally equivalent. However, if you actually want to enter a quoted string as value, then you should use:
key: '"This is the value"'
or escape the double quote:
key: "\"This is the value\""
I gave a quick look at the Psych emitter code, the one invoked by the to_yaml, and there doesn't seem to be an option to force quoting on scalar.
I don't even see the option implemented in the scalar emitter code.
def visit_Psych_Nodes_Scalar o
#handler.scalar o.value, o.anchor, o.tag, o.plain, o.quoted, o.style
end
In other words, you cannot enforce quoting.

Updated for hash conversion
def value_stringify(hash)
hash.each do |k,v|
if v.kind_of? Hash
hash[k]= value_stringify(v)
else
hash[k]="\"#{v}\""
end
end
end
Now use the converted hash to store yaml.
File.open(yaml_file, "w") do |f|
f.write(value_stringify(translations).to_yaml)
end
Now it should work..

The format you get is valid YAML. However, if you really want this you could temporarily modify your data before converting it.
Normal:
{ foo: "bar" }.to_yaml
# => foo: bar
With an space after:
{ foo: "bar " }.to_yaml
# => foo: 'bar '
Note that you get single quotes and not double quotes. So if you temporarily modifying your data you could add in an placeholder which you remove later.
Example:
{ foo: "foo --REMOVE-- ", bar: "bar --REMOVE-- " }.to_yaml
.gsub(' --REMOVE-- ', '')
# => foo: 'foo'
# bar: 'bar'

Related

How to turn a translations table into YAML translation files in ruby on rails

My app is currently using the gem i18n-active_record for translations, storing them in a table translations which has more than 5000 records.
To improve performance, I want to move the translations to YAML files, e.g. en.yml and ar.yml.
I tried File.open(Rails.root+'config/locales/en.yml', 'w') {|f| f.write translation.to_yaml } but the output generated is the following:
raw_attributes:
id: 1
locale: en
key: footer.new_car_make
value: New %{title} Cars
interpolations:
is_proc: 0
created_at: &4 2012-08-15 06:25:59.000000000 Z
updated_at: &5 2012-08-15 06:25:59.000000000 Z
Is there any easy way to make the conversion?
You can try something like this (I don't have any DB backed Rails Translations to try this)
def assign(parts, value, data)
key, *rest = parts
if rest.empty?
data[key] = value
else
data[key] ||= {}
assign(rest, value, data[key])
end
end
translations = {}
Translation.all.each do |translation|
path = [translation.locale] + translation.key.split(".")
assign(path, translation.value, translations)
end
File.write("translations.yml", translations.to_yaml)
Of course you can modify to only export the translations of a single locale or specific keys (by changing the all to a where query).
The code works as following:
It iterates all the Translations, and builds a hash with all the translations.
The key footer.new_car_make of the en translation will end up in a nested hash like:
{
"en" => {
"footer" => {
"new_car_make" => "Whatever text you have in the DB"
}
}
}
and then this hash is saved as YAML format in a file.
The assign method is called with the full key (it contains the locale as well) represented as an array (['en', 'footer', 'new_car_make']) and deconstructs into a head (first value in the array) and the rest (remaining parts).
If the remaining part is empty, it means we've reached the last element and can assign the value, otherwise we add a nested hash and recurse.
You can try this in the console (just copy paste it). As mentioned this might not run right out of the box, let me know if you have troubles.

Parse json key value pair in Rails to format the date

I am trying to format date by iterating through the #data json formatted input shown as below.
#data= JSON.parse(request,symbolize_names: true)[:data]
#data = #data.each{ |key,val| k = Date.parse(key).strftime("%Y %m") }
But this way it is showing error "no implicit conversion of Symbol into String".
Could any one please help?
If you're iterating over a hash where the keys are symbols, then the error is telling you where and what's the problem. In order to parse a date, you must pass a string as argument, and as you haven't converted such object, then you're getting the error.
Try using to_s, to convert the symbol to string:
#data.each do |key, _|
puts Date.parse(key.to_s).strftime '%Y %m'
end
Note if you're inside a block, and you're not going to use the k variable you're creating, then you can avoid creating it, it won't be available outside the block. You're just printing the parsed date.
If you're not going to use the value block variable, then you can omit it.
As pointed #mu, you can omit the symbolize_names: true, and this way the keys will be strings, then, the conversion isn't needed:
require 'date'
require 'json'
request = '{
"data": {
"2017-11-22 00:22:26": "foo" ,
"2017-11-22 00:22:27": "bar"
}
}'
#data = JSON.parse(request)['data']
#data.each do |key, _|
puts Date.parse(key).strftime '%Y %m'
end
request is an approximation to your real data.

Chef, ruby hashes and templates

I know this is more a ruby question than chef, but...
I have some attributes like:
default['my_cookbook']['some_namespace1']['some_attribute1'] = 'some_value1'
default['my_cookbook']['some_namespace1']['some_attribute2'] = 'some_value2'
default['my_cookbook']['some_namespace1']['some_attribute2'] = 'some_value3'
...
default['my_cookbook']['some_namespace2']['some_attribute1'] = 'some_value1'
default['my_cookbook']['some_namespace2']['some_attribute2'] = 'some_value2'
default['my_cookbook']['some_namespace2']['some_attribute2'] = 'some_value3'
...
On the other hand, I am creating a template resource like this:
template 'template_name' do
source 'template_source.erb'
variables (
my_namespace_1: node['my_cookbook']['some_namespace1'],
my_namespace_2: node['my_cookbook']['some_namespace2']
)
end
Then in the template_source.erb I try:
...
<%= #my_namespace_1['some_attribute1'] %> #=> 'some_value1'
...
However when I run Kitchen I get this, instead of 'some_value1':
Chef::Mixin::Template::TemplateError
------------------------------------
undefined method `[]' for nil:NilClass
How should I send the template variable to use it this way?
EDIT: This applies only to Ruby in general and not to Chef in particular.
Pass a nested hash:
template 'template_name' do
source 'template_source.erb'
variables (
my_namespace_1: {
some_attribute1: node['my_cookbook']['some_namespace1']['some_attribute1']
}
)
end
But rather than copying the values verbatim you can use the full power of the Hash class to slice, dice and merge together whatever you want:
template 'template_name' do
source 'template_source.erb'
variables (
node['my_cookbook'].slice('some_namespace1', 'some_namespace2')
)
end
One gotcha in Ruby that you have tripped on is that symbols are usually used as hash keys:
# newer literal syntax
a_hash = {
foo: 'bar'
}
# or with the older hash-rocket syntax
a_hash = {
:foo => 'bar'
}
Symbols are extemly efficient since they are interned strings that are stored in table - when comparing symbols you compare the object ID instead of comparing each character in the string.
In fact strings are only really used when you want keys in the hash that are not valid Ruby symbols - like when building a hash of HTTP headers.
Ruby does not treat symbol and string keys indifferently:
{
foo: 'bar'
}[:foo]
# => bar
{
foo: 'bar'
}['foo']
# => nil
So to access the passed variable in the template you would use:
<%= #my_namespace_1[:some_attribute1] %>
What you have in the example should be working. I am guessing you have a typo somewhere in your original recipe that you corrected when generic-ifying the code.

How to resolve this error erb is throwing when passing a currency formatted string

I'm using a module within my Rails App to perform some actions and render a html file and save it to S3. So far so good, apart from the fact that I need to pass a currency variable to to be rendered and erb is throwing this error:
undefined method `/' for "3,395,000":String
Here's my code:
options = {
...
price: Money.new(#case.cash_price / 100.to_i, "DKK").format.to_s.html_safe,
...
}
And here's my module:
def generate_html(options)
require 'erb'
erb_file = "templates/banners/widesky.html.erb"
erb_str = File.read(erb_file)
...
#price = options[:price]
...
renderer = ERB.new(erb_str)
result = renderer.result(binding)
FileUtils.mkdir_p('temp') unless File.directory?('temp')
File.open('temp/index.html', 'w') do |f|
f.write(result)
end
'temp/index.html'
end
And I tried formatting the currency in different ways, but I always get the same error. Any ideas why?
EDIT
#case.cash_price originally is an Integer. I want to convert it to a string with commas (hence using Money to format it). The problem seems to be that erb doesn't like the formatted result and throw the above error.
If for some reason you cannot use any gem/helper, let's reinvent the wheel!
def to_currency(price_in_cents, currency=nil, decimal_separator = '.', thousand_separator = ',')
price_in_cents.to_s.rjust(3,'0').reverse.insert(2,decimal_separator).gsub(/(\d{3})(?=\d)/, '\1'+thousand_separator).reverse+(currency ? " #{currency}" : '')
end
puts to_currency(123456789, 'DKK')
puts to_currency(123456, '€', ',', ' ')
puts to_currency(1)
It outputs :
1,234,567.89 DKK
1 234,56 €
0.01
Note that price_in_cents should be either a String that looks like an Integer ("123456789") or an Integer (123456789), but not a preformatted String ("123,456.78") or a Float (1.23).
Finally, the resulting String is as unsafe as price_in_cents :
to_currency("unsafe_codejs")
=> "unsafe_code.js"
You don't have to specify html_safe on the result anyway, because nothing would be escaped in "1,234,567.89 DKK".
Original answer :
If cash_price is a String with commas, you need to remove the commas first, then convert it to a float, then divide by 100, and then convert the result to an Integer.
cash_price.to_s is to avoid getting errors if cash_price does come as a Numeric.
price: Money.new((#case.cash_price.to_s.delete(',').to_f/100).to_i, "DKK").format.to_s.html_safe
#case.cash_price is a string so you can't perform any mathematical operations on it. You would need to convert the value to an integer (3395000) rather than a comma delimited string as you currently have ('3,395,000').
A side note, 100.to_i is redundant as 100 is already an integer, unless you wanted to convert the equation to an integer, which would need brackets (#case.cash_price / 100).to_i.

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