rubyrecursionruby-hash

How to produce a concatenated string from a hash recursively?


I have the following complicated hash structure (among many) that looks like the following:

hash = {"en-us"=>
  {:learn_more=>"Learn more",
   :non_apple_desktop=>"To redeem, open this link.",
   :value_prop_safety=>"",
   :storage_size=>
    {:apple_tv_1_month_tms=>
      {:cta=>"Offer",
       :details=>"Get a 1-month subscription!.",
       :disclaimer=>"This offer expires on December 10, 2021.",
       :header=>"Watch The Morning Show ",
       :normal_price=>"$2.99"}
      }
    }
  }

What I'd like to do is to have a function that will produce the following string output based off the hash structure:

en-us.storage_size.apple_tv_1_month_tms.cta: Offer
en-us.storage_size.apple_tv_1_month_tms.details: Get a 1-month subscription!.
en-us.storage_size.apple_tv_1_month_tms.disclaimer: This offer expires on December 10, 2021.
en-us.storage_size.apple_tv_1_month_tms.header: Watch The Morning Show
en-us.storage_size.apple_tv_1_month_tms.normal_price: $2.99
en-us.learn_more: Learn more
en-us.non_apple_desktop: To redeem, open this link.
en-us.value_prop_safety: 

I've used this recursive function from another stackoverflow question that somewhat accomplishes this:

def show(hash, current_path = '')
  string = ''
  hash.each do |k,v|
    if v.respond_to?(:each)
      current_path += "#{k}."
      show v, current_path
    else
      string += "#{current_path}#{k}: #{v}" + "\n"
    end
  end
  string
end

If I place a puts statement in the body of the method I can see the desired result but its line by line. What I need is to obtain the entirety of the output because I will be writing it to a csv. I can't seem to get it to work in its current incarnation.

If I were to place a puts show(hash) into my irb, then I won't get any output. So in summary, I am trying to do the following:

show(hash) ----->

en-us.storage_size.apple_tv_1_month_tms.cta: Offer
en-us.storage_size.apple_tv_1_month_tms.details: Get a 1-month subscription!.
en-us.storage_size.apple_tv_1_month_tms.disclaimer: This offer expires on December 10, 2021.
en-us.storage_size.apple_tv_1_month_tms.header: Watch The Morning Show
en-us.storage_size.apple_tv_1_month_tms.normal_price: $2.99
en-us.learn_more: Learn more
en-us.non_apple_desktop: To redeem, open this link.
en-us.value_prop_safety: 

This should be an easy recursive task but I can't pinpoint what exactly I've got wrong. Help would be greatly appreciated. Thank you.


Solution

  • In my opinion it is much more convenient to use i18n gem

    It has I18n::Backend::Flatten#flatten_translations method. It receives a hash of translations (where the key is a locale and the value is another hash) and return a hash with all translations flattened, just as you need

    Just convert the resulting hash to a string and you're done

    require "i18n/backend/flatten"
    
    include I18n::Backend::Flatten
    
    locale_hash = {"en-us"=>
      {:learn_more=>"Learn more",
       :non_apple_desktop=>"To redeem, open this link.",
       :value_prop_safety=>"",
       :storage_size=>
        {:apple_tv_1_month_tms=>
          {:cta=>"Offer",
           :details=>"Get a 1-month subscription!.",
           :disclaimer=>"This offer expires on December 10, 2021.",
           :header=>"Watch The Morning Show ",
           :normal_price=>"$2.99"}
          }
        }
      }
    
    
    puts flatten_translations(nil, locale_hash, nil, nil).
           map { |k, v| "#{k}: #{v}" }.
           join("\n")
    
    # will print
    # en-us.learn_more: Learn more
    # en-us.non_apple_desktop: To redeem, open this link.
    # en-us.value_prop_safety: 
    # en-us.storage_size.apple_tv_1_month_tms.cta: Offer
    # en-us.storage_size.apple_tv_1_month_tms.details: Get a 1-month subscription!.
    # en-us.storage_size.apple_tv_1_month_tms.disclaimer: This offer expires on December 10, 2021.
    # en-us.storage_size.apple_tv_1_month_tms.header: Watch The Morning Show 
    # en-us.storage_size.apple_tv_1_month_tms.normal_price: $2.99
    

    Of course it's better to include not in main object, but in some service object

    require "i18n/backend/flatten"
    
    class StringifyLocaleHash
      include I18n::Backend::Flatten
    
      attr_reader :locale_hash
    
      def self.call(locale_hash)
        new(locale_hash).call
      end
    
      def initialize(locale_hash)
        @locale_hash = locale_hash
      end
    
      def call
        flatten_translations(nil, locale_hash, nil, nil).
          map { |k, v| "#{k}: #{v}" }.
          join("\n")
      end
    end
    
    # To get string call such way
    StringifyLocaleHash.(locale_hash)