Access YAML list in Rails translation - ruby-on-rails

I have the following translation file:
ru:
common:
age:
- год
- года
- лет
and I want to access list items in tranlsation. I tried something like:
t('common.age[2]')
But it doesn't work. How to do it properly?

You can store an array structure in YAML but you can't iterate over one in YAML-land. YAML is a data serialisation language, and is not meant to contain executable statements (Array#[] is a Ruby method call), only data structures. Executable statements are the responsibility of the programming language you're working with, in your case Ruby.
So, in your case, you need to use t('common.age') to first pull out the array from YAML, then iterate over it in Ruby-land:
array = t('common.age')
# => ["год", "года", "лет"]
array[2]
# => "лет"

Related

How to iterate over deep nested hash without known depth in Ruby

I have multiple YAML (localization) files. I parse them and convert to hash in Ruby.
For example this is one of them:
hello: Hallo
messages:
alerts:
yay: Da!
no: Nein
deep:
nested:
another:
level:
hi: Hi!
test: Test!
Basically, this is look like a locale file in Rails App using YAML.
What I want to do is iterate this Hash recursively and get key and value. So that i can translate values one-by-one from API Endpoint like Google Translate. I want to keep nested hashes in same schema so that Rails can find by keys.
I know i can use nested loops but there is no guarantee that nested hashes is a known number of. How can I iterate this hash recursively so i can manipulate values (translate/replace)?
Expected Result: (after used translation service from API call)
hello: Hello
messages:
alerts:
yay: Yup!
no: No
deep:
nested:
another:
level:
hi: Hi!
test: Test!
What I've tried so far:
hash = YAML.load('de.yml') # parse source Deutsch locale
new_hash = {}
hash.each |key, value| do
new_hash[key] = translate_func(value) # here... translate value then assign very same key including parents.
# Do more loops....
end
# Now write this new_hash to yaml file...
But this only manipulate hello only. To get work with others I have to make a loop. But how many keys are nested is unknown.
How can I iterate over all values of locale hash and keep the schema intact?
And if possible but not mandatory, I would be very happy if we can keep the order of keys on final result. That would be awesome to find missing keys later when manually reviewed.
I am very new to ruby.
I am using Ruby 2.7.2
Conclusion / Resolve
All answers are correct and I love all of them. However, I would like to be able to control both keys and values. Not just transform by values. Therefore, I accepted an answer that fits to my needs. I was able to do my intention with selected answer.
You can use deep_transform_values! on your hash object to change the values recursively. (Or its non-destructive version deep_transform_values which returns a new hash instead of changing the original hash.)
hash.deep_transform_values! { |value| translate_func(value) }
Note: deep_transform_values! is a Rails method. See the source code here for inspiration if you're not using Rails.
If you want a simple non-rails solution then you can just create a recursive method:
def recurse(hash)
hash.transform_values do |v|
case v
when String
v.reverse # Just for the sake of the example
when Hash
recurse(v)
else
v
end
end
end
Output:
{"hello"=>"ollaH", "messages"=>{"alerts"=>{"yay"=>"!aD", false=>"nieN"}, "deep"=>{"nested"=>{"another"=>{"level"=>{"hi"=>"!iH"}}}}}, "test"=>"!tseT"}
However this might be a case of reinventing the wheel - you can use the i18n gem for translations and i18n-tasks to prefill your YAML files with translations from the Google Translate API.
So, you want to parse recursively until there are no more levels to parse into.
It’s super common in software and referred to as “recursion”. Have a search around to learn more about it - it’ll come up again and again in your journey. Welcome to ruby btw!
As for your actual current problem. Have a read of https://mrxpalmeiras.wordpress.com/2017/03/30/how-to-parse-a-nested-yaml-config-file-in-python-and-ruby/
But also, consider the i18n gem. See this answer https://stackoverflow.com/a/51216931/1777331 and the docs for the gem https://github.com/ruby-i18n/i18n This might fix your problem of handling internationalisation without you having to get into the details of handling yaml files.

Rails serialize not storing correctly

I am setting up stripe connect with the example from https://github.com/rfunduk/rails-stripe-connect-example and am running into a problem using serialize to store stripe_account_status which should be stored as an array.
This is how it should be stored (Generated from the above example link)
{"details_submitted"=>false, "charges_enabled"=>true, "transfers_enabled"=>false, "fields_needed"=>["legal_entity.first_name", "legal_entity.last_name", "legal_entity.dob.day", "legal_entity.dob.month", "legal_entity.dob.year", "legal_entity.address.line1", "legal_entity.address.city", "legal_entity.address.postal_code", "bank_account"], "due_by"=>nil}
And this is how my application is storing it
{:details_submitted=>false, :charges_enabled=>true, :transfers_enabled=>false, :fields_needed=>["legal_entity.first_name", "legal_entity.last_name", "legal_entity.dob.day", "legal_entity.dob.month", "legal_entity.dob.year", "legal_entity.address.line1", "legal_entity.address.city", "legal_entity.address.postal_code", "bank_account"], :due_by=>nil}
As far as I am concerned everything is set up the same. The only difference is that the first example uses
serialize :stripe_account_status, JSON
and my app just has
serialize :stripe_account_status
The reason for this is that when I add JSON I this error:
JSON::ParserError - 795: unexpected token at '':
I have tried finding out the JSON error including changing the config/initializers/cookies_serializer.rb to use :hybrid but this is giving me the same error.
Could someone point me into the right direction of either fixing the JSON issue OR finding a way to make sure the stripe_account_status is stored as an array correctly.
Below is the methods used to store the array:
if #account
user.update_attributes(
currency: #account.default_currency,
stripe_account_type: 'managed',
stripe_user_id: #account.id,
secret_key: #account.keys.secret,
publishable_key: #account.keys.publishable,
stripe_account_status: account_status
)
end
def account_status
{
details_submitted: account.details_submitted,
charges_enabled: account.charges_enabled,
transfers_enabled: account.transfers_enabled,
fields_needed: account.verification.fields_needed,
due_by: account.verification.due_by
}
end
Thanks I really appreciate any direction you could point me!
When you ask Rails to serialize an attribute on a model, it will default to storing the object as YAML string.
You can ask Rails to serialize differently, as you have noticed by providing a class to do the serialization e.g
serialize :stripe_account_status, JSON
The reason why this isn't working when you add it is because you presumably already have a record in the database using the YAML and so Rails can't parse this as a valid JSON string when reading from the DB. If it's just development data that you don't need, you can delete the records and then use JSON, otherwise you will need to convert the current YAML strings to JSON.
Rails will also symbolize the keys of a hash when parsing a serialized string in the database. This is the only difference between the hashes in your question and shouldn't matter in practise. Should you need String keys for some reason, you can use the #stringify_keys method on the hash provided by Rails.

How can I get a hash of the contents of a Rails fixture instance?

This is using Rails 4.2.0, Ruby 2.2.0.
What I'd like to do is use the data contained in a fixture object to verify that duplicates are caught before insertion into the same database:
test "identical entries should be impossible to create" do
dup_entry = Entry.new(entries(:test_entry))
assert_not dup_entry.save
end
(where Entry is a well-defined model with a controller method .new, and test_entry is a fixture containing some valid Entry instance.)
Unfortunately, this doesn't work because entries(:test_entry) is going to be an Entry, not a hash accepted by Entry.new.
I know that I can access fixture properties with an expression of the form fixture_objname.property in the associated tests, since whatever is specified in the YAML will automatically be inserted into the database and loaded. The problem with this is that I have to manually retype a bunch of property names for the object I just specified in the YAML, which seems silly.
The documentation also says I can get the actual model instances by adding self.use_instantiated_fixtures = true to the test class. However, there don't seem to be any instance_methods that will dump out the fixture's model instance (test_entry) in a hash format to feed back into the .new method.
Is there an idiomatic way to get what I want, or a different, easier way?
I believe you're looking for something like:
entries(:test_entry).attributes
entries(:test_entry).attributes.class # => Hash
You can also remove properties if needed:
entries(:admin).attributes.except("id")
Hope this helps.

Ruby getting deeply nested JSON API data

I have a rails app which gets a response from World Weather Online API. I'm using the rest-client gem and the response is in JSON format.
I parse the response using:
parsed_response = JSON.parse(response)
Where parsed_response is obviously a hash.
The data I need are strings inside a hash inside an array inside a hash inside another array inside another hash inside another hash.
The inner-most nested hashes are inside ["hourly"], an array of 8 hashes, each with 20 keys, possessing string values of various weather parameters. Each of these hashes in the array is a different time of day (the forecast is three-hourly, 3*8 = 24hours).
So, for example, if I want the swell height in metres at 9pm, I find it with the following call:
#swell_height = parsed_data["data"]["weather"][0]["hourly"][7]["swellHeight_m"]
Where the 7th element in the array correspond to "time" => "2100"
While I can definitely work with this, I'm curious as to whether there is a more straightforward method of accessing my data, like if it was a database table, I could use active record, something like:
#swell_height = parsed_data.swellHeight_m.where(:time => "2100")
You may want to look at JSONPath. It does exactly what you need. Its syntax is very similar to XPath, but JSONPath works with JSON data (as obvious).
There is a Ruby implementation: https://github.com/joshbuddy/jsonpath
I personally use it in every project where I need to test JSON responses.

Is it okay to use strings everywhere and never use symbols in Ruby on Rails?

There are some annoyances with using symbols in hashes. For example, the JSON gem that we use always returns strings from any JSON string that's parsed, so wherever we reference a hash generated from decoding JSON, we have to use a combination of strings and symbols to access hashes.
Style-wise, is it ok to keep things consistent throughout by using strings only?
Strings are mutable, hence each time you reference "foo" ruby creates a new object. You can test that by calling "foo".object_id in irb. Symbols, on the other hand, are not, so each time you reference :foo ruby returns the same object.
Regarding the "style" and "consistency" you can always use hash.symbolize_keys! for your received json data, this will turn all string keys into symbols. And vice-versa - hash.stringify_keys! to make them strings again.
There is no rule that says a hash key should be a symbol.
The symbol-as-key is seen a lot in Rails as a convention ... Rails makes a lot of use of passing hashes to allow multiple parameters, and the keys in such hashes are often symbols to indicate that they are expected/permissible parameters to a method call.
For the indecisive among us:
http://as.rubyonrails.org/classes/HashWithIndifferentAccess.html

Resources