24

I've been trying to sort an i18n translations YAML file with Ruby so I can manage new translations in a better and organized way, but I've been wondering if there is something to ease the task.

I found a YAML file writer so I can write a hash into a file, but my problem is to sort the hash correctly. If I got hash h, h.sort returns an array and I still haven't figured a simple way to do that.

I have YAML files like this:

pt-br:    
  global:
    misc:
      total: "Total"
      all: "Todos"
      close: "Fechar"
      cancel: "Cancelar"

    crud:
      access: "Acessar"
      back: "Voltar"
      edit: "Editar"
      confirm: "Confirmar"
      send: "Enviar"

...

(The files are way larger than this)

But I want to sort them this way:

pt-br:    
  global:
    crud:
      access: "Acessar"
      back: "Voltar"
      confirm: "Confirmar"
      edit: "Editar"
      send: "Enviar"

    misc:
      all: "Todos"
      cancel: "Cancelar"
      close: "Fechar"          
      total: "Total"

I thought that some simple recursive method could help me like this:

def translation_sort(h)
  if h.class == Hash
    h = h.sort
    h.each{|item| translation_sort(item)}
  end
  h
end

require "yaml"
h=YAML.load_file(File.open("~/pt-br.sample.yml"))
translation_sort(h)
1
  • is there any sublime plugin for the same? Commented Jul 30, 2015 at 7:54

7 Answers 7

11

In my use cases where deep sorting a hash is needed, the hash is always a tree where keys are labels and values are (sub)trees (if hashes) or leaves (otherwise). I need to deep-sort only the labels of trees (not the values).

I got this

before: {"a":[2,10,{"5":null,"1":null,"3":null}],"x":{"5":null,"1":null,"3":null},"a2":{"5":[2,10,5],"1":null,"3":null}}
after:  {"a":[2,10,{"5":null,"1":null,"3":null}],"a2":{"1":null,"3":null,"5":[2,10,5]},"x":{"1":null,"3":null,"5":null}}

with this

    def deeply_sort_hash(object)
      return object unless object.is_a?(Hash)
      hash = Hash.new
      object.each { |k, v| hash[k] = deeply_sort_hash(v) }
      sorted = hash.sort { |a, b| a[0].to_s <=> b[0].to_s }
      hash.class[sorted]
    end
2
  • I stumbled upon this answer when looking for a builtin method (or an ActiveSupport one). Since there does not seem to be one yet, I was very happy to find this response. I just want to leave a comment here how to achieve the same thing in newer ruby versions with fewer lines: def hash_deep_sort(element); return element unless element.is_a?(Hash); hash = element.transform_values { |value| hash_deep_sort(value) }; hash.sort.to_h; end (This is not supposed to be a oneliner, it's just stackoverflow forcing me not to use multiple lines in a comment...) Commented Aug 14, 2020 at 6:26
  • Confirmed to work in Ruby 2.7.1 without any changes.
    – mmell
    Commented Dec 3, 2020 at 18:27
9

You shouldn't use the YAML library like suggested in the other answers. It will screw up the formatting of long string values, remove your comments and spit unreadable char escapes when you use accents and special characters (which you will, since you are doing i18n). Use this gem I created:

https://github.com/redealumni/i18n_yaml_sorter

It will only sort the lines on the file, so everything will remain the same way it was on the original yaml (your accents, the YAML construct you used to enter the strings, indentation, etc). It will work with deeply nested yamls and results are pretty solid. The gem includes tests and it's good for ruby 1.8 or 1.9.

It comes with a TextMate Bundle (Shift + Command + S) and a Rails rake task so you can sort the files easily and instantly in your editor. It's really fast.

To illustrate the difference:

Original:

  pt-BR:
    # Note how this is a nice way of inputing
    # paragraphs of text in YAML. 
    apples: >
      Maçãs são boas,
      só não coma 
      seus iPods!
    grapes: Não comemos elas.
    bananas: |
      Bananas são "legais":
        - Elas são <b> doces </b>.
        isto: não é chave

      Por isto todos gostam de bananas!

Results by YAML::dump :

  pt-BR: 
    apples: "Ma\xC3\xA7\xC3\xA3s s\xC3\xA3o boas, s\xC3\xB3 n\xC3\xA3o coma  seus iPods!\n"
    bananas: "Bananas s\xC3\xA3o \"legais\":\n  - Elas s\xC3\xA3o <b> doces </b>.\n  isto: n\xC3\xA3o \xC3\xA9 chave\n\n\ Por isto todos gostam de bananas!\n"
    grapes: "N\xC3\xA3o comemos elas."

Results by i18n_yaml_sorter:

  pt-BR:
    # Note how this is a nice way of inputing
    # paragraphs of text in YAML. 
    apples: >
      Maçãs são boas,
      só não coma 
      seus iPods!
    bananas: |
      Bananas são "legais":
        - Elas são <b> doces </b>.
        isto: não é chave

      Por isto todos gostam de bananas!
    grapes: Não comemos elas.
5
  • I just tried this gem for sorting a config.yml file and it didn't work. The result was sorted BUT also different.
    – aercolino
    Commented Nov 13, 2012 at 18:31
  • 1
    Oops, I slipped on this gem again, after many years. The reason why this gem doesn't work is because it ignores references, which means dependencies are not correctly sorted. To make your YAML file resilient to key sorting, rename named keys in a way that they'll come before being referenced. EG: (1) First I renamed some_key: &some_name to _some_key: &some_name and added some_key: \n <<: *some_name. (2) Then I moved all named keys and their values to the top of the file, and added numbers to keys where needed. Notice that this manual adjustment is only needed once.
    – aercolino
    Commented Jun 6, 2016 at 14:22
  • 1
    i18n_yaml_sorter appears to no longer work github.com/redealumni/i18n_yaml_sorter/issues/16
    – Tim Hughes
    Commented Aug 13, 2020 at 10:05
  • 2
    The entire repo was deleted or made private. Commented Jun 7, 2022 at 21:34
  • Down voting since the repo is no more available ... :/
    – ZedTuX
    Commented Jun 23, 2022 at 12:55
6

UPDATE April 2014:

Using Rails 3.2.13, Ruby 1.9.3p489:

I just used the i18n_yaml_sorter gem ( https://github.com/redealumni/i18n_yaml_sorter ).

Simply add to your Gemfile:

gem 'i18n_yaml_sorter', group: :development

Then run the rake task to sort your locales' files:

rake i18n:sort

Worked perfectly, even though the gem has been last authored 2 years ago.

2

In Ruby 1.8 hashes don't have a particular order, so you cannot just sort them.

You could monkey-patch/overwrite the to_yaml method of Hash like this:

#!/usr/local/bin/ruby -w

require 'yaml'

class Hash
  def to_yaml(opts = {})
    YAML::quick_emit(self, opts) do |out|
      out.map(taguri, to_yaml_style) do |map|
        keys.sort.each do |k|
          v = self[k]
          map.add(k, v)
        end
      end
    end
  end
end

dict = YAML.load($<.read)

puts dict.to_yaml

Of course, the exact details may depend on your version of YAML/Ruby. The example above is for Ruby 1.8.6.

1
  • YAML::quick_emit is deprecated and is no longer available. Commented May 14, 2019 at 19:35
1

Here's another alternative for anyone else who comes across this..

require 'yaml'

yaml = YAML.load(IO.read(File.join(File.dirname(__FILE__), 'example.yml')))

@yml_string = "---\n"

def recursive_hash_to_yml_string(hash, depth=0)
  spacer = ""
  depth.times { spacer += "  "}
  hash.keys.sort.each do |sorted_key|
    @yml_string += spacer + sorted_key + ": "
    if hash[sorted_key].is_a?(Hash)
      @yml_string += "\n"
      recursive_hash_to_yml_string(hash[sorted_key], depth+1)
    else
      @yml_string += "#{hash[sorted_key].to_s}\n"
    end
  end
end

recursive_hash_to_yml_string(yaml)

open(File.join(File.dirname(__FILE__), 'example.yml'), 'w') { |f|
  f.write @yml_string
}
1

This is a dated question, but if you're reading this in 2023 in search of an easy way to sort yaml documents, a good tool to use is yq.

https://mikefarah.gitbook.io/yq/operators/sort

You can install it on most platforms and sort a list by a specific key with the following code:

Assume a file called list.yaml with the following content:

- name: foo
  type: bar
- name: pineapple
  type: string
- name: apple
  type: boolean

cat list.yaml | yq 'sort_by(.name)'

This will output a valid yaml document where the list is sorted by the name key.

- name: apple
  type: boolean
- name: foo
  type: bar
- name: pineapple
  type: string
0

Unfortunately YAML::quick_emit has been deprecated and is no longer available in newer builds of the Psych gem. If you want your hash keys to be sorted when serialized to yaml, you'll have to use the following monkey patch instead:

class Hash
    def to_yaml opts={}
        return Psych.dump(self.clone.sort.to_h)
    end
end

Not the answer you're looking for? Browse other questions tagged or ask your own question.