i18n_yaml_editor 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 59360f083d7e5071393e6605905f034df8340f64
4
+ data.tar.gz: 422e871b2b24a8061dc75f12721478e6c3e9c4a7
5
+ SHA512:
6
+ metadata.gz: 469c35174ee3ad8f52526830da4ed069fbdb5e4ad37de79dc770d0a5daaf58efc351c98b4e1622f85e24f64a283af1b49f1b49a4d87aa82c49e71e96ef15a436
7
+ data.tar.gz: e50602ce147daf56bb1837c13678670e86dc46e5848b2cc6824293bb267e4d5ac4b8036579f3c6204836c174aa8dd2e9fc75c798ce1a8ac10841b5cb75067cec
data/README.md ADDED
@@ -0,0 +1,60 @@
1
+ # I18n Yaml Editor
2
+
3
+ [![Build Status](https://travis-ci.org/Sage/i18n_yaml_editor.svg?branch=master)](https://travis-ci.org/Sage/i18n_yaml_editor) [![Maintainability](https://api.codeclimate.com/v1/badges/f71fb2039dd7d50eb90d/maintainability)](https://codeclimate.com/github/Sage/i18n_yaml_editor/maintainability) [![Test Coverage](https://api.codeclimate.com/v1/badges/f71fb2039dd7d50eb90d/test_coverage)](https://codeclimate.com/github/Sage/i18n_yaml_editor/test_coverage) [![Documentation Status](http://inch-ci.org/github/Sage/i18n_yaml_editor.svg?branch=master)](http://inch-ci.org/github/Sage/i18n_yaml_editor) [![MIT licensed](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/Sage/i18n_yaml_editor/blob/master/LICENSE) [![Dependency Status](https://gemnasium.com/badges/github.com/Sage/i18n_yaml_editor.svg)](https://gemnasium.com/github.com/Sage/i18n_yaml_editor)
4
+
5
+ I18n Yaml Editor is based on [IYE](https://github.com/firmafon/iye).
6
+ I18n Yaml Editor makes it easy to translate your Rails I18N files and keep them up to date.
7
+ It uses YAML files directly, so you don't need to keep a separate database in sync.
8
+ This has several benefits:
9
+
10
+ * Branching and diffing is trivial
11
+ * It does not alter the workflow for developers etc., whom can continue editing the
12
+ YAML files directly
13
+ * If your YAML files are organized in subfolders, this structure is kept intact
14
+
15
+ ![I18n Yaml Editor](https://cloud.githubusercontent.com/assets/1446195/10295880/1f829dd6-6bc4-11e5-9a08-bb79d9864bdb.png)
16
+
17
+ ## Prerequisites
18
+
19
+ You need to understand a few things about I18n Yaml Editor for it to make sense, mainly:
20
+
21
+ * I18n Yaml Editor does not create new keys - keys must exist for at least one locale in the YAML files
22
+ * I18n Yaml Editor does not create new locales - at least one key must exist for each locale in the YAML files
23
+
24
+ ## Workflow
25
+
26
+ 1. Install I18n Yaml Editor:
27
+
28
+ $ git clone git@github.com:Sage/i18n_yaml_editor.git
29
+ $ cd i18n_yaml_editor
30
+ $ gem build i18n_yaml_editor.gemspec
31
+ $ gem install i18n_yaml_editor-1.3.1.gem
32
+
33
+ 2. The `i18n_yaml_editor` executable is now available, use it wherever you want.
34
+
35
+ $ i18n_yaml_editor path/to/i18n/locales [port]
36
+
37
+ At this point I18n Yaml Editor loads all translation keys for all locales, and creates any
38
+ keys that might be missing for existing locales, the default port is 5050
39
+
40
+ 3. Point browser at [http://localhost:5050](http://localhost:5050)
41
+ 4. Make changes and press 'Save' - each time you do this, all the keys will be
42
+ written to their original YAML files.
43
+ 5. Review your changes before committing files, e.g. by using `git diff`.
44
+
45
+ ## Development
46
+
47
+ ### Getting started
48
+ $ git clone git@github.com:Sage/i18n_yaml_editor.git
49
+ $ cd i18n_yaml_editor
50
+ $ gem install bundler
51
+ $ bundle install
52
+ $ SIMPLE_COV=true bundle exec guard
53
+
54
+
55
+ ## License
56
+
57
+ This gem is available as open source under the terms of the
58
+ [MIT licence](LICENSE).
59
+
60
+ Copyright (c) 2018 Sage Group Plc. All rights reserved.
data/Rakefile ADDED
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rake/testtask'
4
+ require 'rubocop/rake_task'
5
+ require 'capture_io/capture_io'
6
+
7
+ Rake::TestTask.new do |t|
8
+ t.libs << 'test'
9
+ t.libs << 'lib'
10
+ t.pattern = 'test/**/test_*.rb'
11
+ end
12
+
13
+ # Mixin
14
+ module RuboCop
15
+ # Mixin
16
+ class RakeTask
17
+ private
18
+
19
+ def run_cli(verbose, options)
20
+ require 'rubocop'
21
+
22
+ cli = CLI.new
23
+ puts "\033[1;33m# Running RuboCop...\033[0m" if verbose
24
+ result = cli.run(options)
25
+ failed = result.nonzero? && fail_on_error
26
+ abort("\033[0;31mRuboCop failed!\033[0m\n") if failed
27
+ puts "\033[0;32mRuboCop passed\033[0m\n\n\n"
28
+ end
29
+ end
30
+ end
31
+
32
+ desc 'Run RuboCop'
33
+ RuboCop::RakeTask.new(:rubocop)
34
+
35
+ desc 'Generate documentation in doc/ and check documentation coverage'
36
+ task :yardoc do
37
+ require 'yard'
38
+ puts "\n\033[1;33m# Running Yardoc...\033[0m"
39
+ yard = `yard stats --list-undoc --compact`
40
+ if yard =~ /Undocumented Objects/
41
+ puts yard
42
+ puts "\033[0;31mYardoc failed - documentation coverage < 100%\033[0m\n"
43
+ exit 1
44
+ else
45
+ puts yard.each_line.to_a[0..-2]
46
+ puts "\033[0;32mYardoc passed - 100.00% documented\033[0m\n\n\n"
47
+ end
48
+ end
49
+
50
+ task :coverage do
51
+ puts "\033[1;33m# Running Minitest...\033[0m"
52
+ out, = capture_io do
53
+ puts `bundle exec rake test`
54
+ end
55
+ puts out
56
+ err = /\d+ tests, \d+ assertions, (\d+) failures, (\d+) errors, \d+ skips/
57
+ unless out.scan(err).flatten.map(&:to_i).reduce(&:+).zero?
58
+ puts "\n\033[0;31mMinitest failed\033[0m\n"
59
+ exit 1
60
+ end
61
+
62
+ if out.lines.last =~ /LOC \(100\.0%\) covered/ ||
63
+ out.lines[-2] =~ /Coverage is at 100\.0%/
64
+ puts "\033[0;32mMinitest passed - 100.00% covered\033[0m\n\n"
65
+ else
66
+ puts "\n\033[0;31mMinitest failed - Code coverage < 100%\033[0m\n"
67
+ exit 1
68
+ end
69
+ end
70
+
71
+ task default: %i[coverage yardoc rubocop] do
72
+ puts "\033[0;32mYAY! - I18n Yaml Editor test suite passed\033[0m\n\n"
73
+ end
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'i18n_yaml_editor'
5
+
6
+ i18n_yaml_editor = I18nYamlEditor::App.new(*ARGV)
7
+ i18n_yaml_editor.start
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ # rubocop:disable Metrics/BlockLength
4
+ Gem::Specification.new do |s|
5
+ s.name = 'i18n_yaml_editor'
6
+ s.version = '2.0.0'
7
+ s.date = '2015-10-07'
8
+ s.summary = 'I18n Yaml Editor'
9
+ s.email = 'wolfgang.teuber@sage.com'
10
+ s.homepage = 'http://github.com/Sage/i18n_yaml_editor'
11
+ s.description = 'I18n Yaml Editor'
12
+ s.authors = ['Harry Vangberg', 'Wolfgang Teuber']
13
+ s.executables << 'i18n_yaml_editor'
14
+ s.files = [
15
+ 'README.md',
16
+ 'Rakefile',
17
+ 'i18n_yaml_editor.gemspec',
18
+ 'bin/i18n_yaml_editor',
19
+ 'lib/i18n_yaml_editor.rb',
20
+ 'lib/i18n_yaml_editor/app.rb',
21
+ 'lib/i18n_yaml_editor/category.rb',
22
+ 'lib/i18n_yaml_editor/core_ext.rb',
23
+ 'lib/i18n_yaml_editor/key.rb',
24
+ 'lib/i18n_yaml_editor/store.rb',
25
+ 'lib/i18n_yaml_editor/filter.rb',
26
+ 'lib/i18n_yaml_editor/update.rb',
27
+ 'lib/i18n_yaml_editor/cast.rb',
28
+ 'lib/i18n_yaml_editor/transformation.rb',
29
+ 'lib/i18n_yaml_editor/translation.rb',
30
+ 'lib/i18n_yaml_editor/web.rb',
31
+ 'views/categories.html.erb',
32
+ 'views/debug.html.erb',
33
+ 'views/layout.erb',
34
+ 'views/translations.html.erb'
35
+ ]
36
+ s.test_files = [
37
+ 'test/test_helper.rb',
38
+ 'test/unit/test_app.rb',
39
+ 'test/unit/test_category.rb',
40
+ 'test/unit/test_key.rb',
41
+ 'test/unit/test_store.rb',
42
+ 'test/unit/test_transformation.rb',
43
+ 'test/unit/test_translation.rb'
44
+ ]
45
+ s.add_dependency 'activesupport', '>= 4.0.2'
46
+ s.add_dependency 'cuba', '>= 3'
47
+ s.add_dependency 'psych', '>= 1.3.4'
48
+ s.add_dependency 'tilt', '>= 1.3'
49
+
50
+ s.add_development_dependency 'awesome_print'
51
+ s.add_development_dependency 'cane'
52
+ s.add_development_dependency 'flay'
53
+ s.add_development_dependency 'flog'
54
+ s.add_development_dependency 'github-markup'
55
+ s.add_development_dependency 'guard'
56
+ s.add_development_dependency 'guard-minitest'
57
+ s.add_development_dependency 'guard-rubocop'
58
+ s.add_development_dependency 'inch'
59
+ s.add_development_dependency 'minitest-line'
60
+ s.add_development_dependency 'minitest-reporters'
61
+ s.add_development_dependency 'pry-byebug'
62
+ s.add_development_dependency 'pry-doc'
63
+ s.add_development_dependency 'rack-test'
64
+ s.add_development_dependency 'rake'
65
+ s.add_development_dependency 'redcarpet'
66
+ s.add_development_dependency 'rubocop'
67
+ s.add_development_dependency 'simplecov'
68
+ s.add_development_dependency 'yard'
69
+ end
70
+ # rubocop:enable Metrics/BlockLength
@@ -0,0 +1,125 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'psych'
4
+ require 'yaml'
5
+ require 'active_support/all'
6
+
7
+ require 'i18n_yaml_editor/web'
8
+ require 'i18n_yaml_editor/store'
9
+ require 'i18n_yaml_editor/core_ext'
10
+
11
+ module I18nYamlEditor
12
+ # App provides I18n Yaml Editor's top-level functionality:
13
+ # * Starting I18n Yaml Editor
14
+ # * Loading Translation files
15
+ # * Saving Translation files
16
+ class App
17
+ attr_accessor :store
18
+
19
+ def initialize(path, port = 5050)
20
+ @path = File.expand_path(path)
21
+ @port = port || 5050
22
+ @store = Store.new
23
+ I18nYamlEditor.app = self
24
+ end
25
+
26
+ # Starts I18n Yaml Editor server
27
+ def start
28
+ raise "File #{@path} not found." unless File.exist?(@path)
29
+ $stdout.puts " * Loading translations from #{@path}"
30
+ load_translations
31
+
32
+ $stdout.puts ' * Creating missing translations'
33
+ store.create_missing_keys
34
+
35
+ $stdout.puts " * Starting I18n Yaml Editor at port #{@port}"
36
+ Rack::Server.start app: Web, Port: @port
37
+ end
38
+
39
+ # Loads translations from a given path
40
+ def load_translations
41
+ files = if File.directory?(@path)
42
+ Dir[@path + '/**/*.yml']
43
+ elsif File.file?(@path)
44
+ detect_list_or_file @path
45
+ else
46
+ # rubocop:disable Style/StderrPuts
47
+ $stderr.puts 'No valid translation file given'
48
+ # rubocop:enable Style/StderrPuts
49
+ []
50
+ end
51
+ update_store files
52
+ end
53
+
54
+ # Write the given translations to the appropriate YAML file
55
+ # example translations:
56
+ # {"en.day_names"=>"Mon\r\nTue", "en.session.new.password"=>"Password"}
57
+ def save_translations(translations)
58
+ store.update(translations)
59
+ changes = files(translations: translations)
60
+ changes.each do |file, yaml|
61
+ File.open(file, 'w', encoding: 'utf-8') do |f|
62
+ f.puts normalize(yaml)
63
+ end
64
+ end
65
+ end
66
+
67
+ private
68
+
69
+ # Enables I18n Yaml Editor to deal with inputs that reference a file list or
70
+ # a single translation file
71
+ def detect_list_or_file(path)
72
+ file = YAML.load_file(path)
73
+ file.is_a?(Hash) ? [path] : File.read(path).split
74
+ end
75
+
76
+ def update_store(files)
77
+ files.each do |file|
78
+ if File.exist?(file)
79
+ yaml = YAML.load_file(file)
80
+ store.from_yaml(yaml, file)
81
+ end
82
+ end
83
+ end
84
+
85
+ def files(translations: {})
86
+ store.to_yaml.select do |_, i18n_hash|
87
+ translations.keys.any? do |i18n_key|
88
+ key_in_i18n_hash? i18n_key, i18n_hash
89
+ end
90
+ end
91
+ end
92
+
93
+ def key_in_i18n_hash?(i18n_key, i18n_hash)
94
+ !i18n_key.split('.').inject(i18n_hash) do |hash, k|
95
+ begin
96
+ hash[k]
97
+ rescue StandardError
98
+ {}
99
+ end
100
+ end.nil?
101
+ end
102
+
103
+ def normalize(yaml)
104
+ i18n_yaml = yaml.with_indifferent_access.to_hash.to_yaml
105
+ i18n_yaml_lines = i18n_yaml.split(/\n/).reject { |e| e == '' }[1..-1]
106
+ normalize_empty_lines(i18n_yaml_lines) * "\n"
107
+ end
108
+
109
+ def normalize_empty_lines(i18n_yaml_lines)
110
+ yaml_ary = []
111
+ i18n_yaml_lines.each_with_index do |line, idx|
112
+ yaml_ary << line
113
+ yaml_ary << '' if add_empty_line?(i18n_yaml_lines, line, idx)
114
+ end
115
+ yaml_ary
116
+ end
117
+
118
+ def add_empty_line?(process, line, idx)
119
+ return if process[idx + 1].nil?
120
+ this_line_spcs = line[/\A\s*/].length
121
+ next_line_spcs = process[idx + 1][/\A\s*/].length
122
+ this_line_spcs - next_line_spcs > 2
123
+ end
124
+ end
125
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module I18nYamlEditor
4
+ # Transformation provides
5
+ module Cast
6
+ # Lists all supported types of conversions
7
+ TYPES = %i[numeric boolean array].freeze
8
+
9
+ # Contains a check method for each supported type
10
+ CHECK = {
11
+ numeric: ->(klass) { klass < Numeric },
12
+ boolean: ->(klass) { [TrueClass, FalseClass].include?(klass) },
13
+ array: ->(klass) { klass == Array }
14
+ }.freeze
15
+
16
+ # Contains a conversion method for each supported type
17
+ CONVERT = {
18
+ numeric: lambda do |value|
19
+ num = BigDecimal(value)
20
+ num.frac.zero? ? num.to_i : num.to_f
21
+ end,
22
+ boolean: ->(value) { value.casecmp('true').zero? },
23
+ array: ->(value) { value.split("\r\n") }
24
+ }.freeze
25
+
26
+ # Converts a given value to a specific data type
27
+ def cast(klass, value)
28
+ TYPES.each do |type|
29
+ return CONVERT[type].call(value) if CHECK[type].call(klass)
30
+ end
31
+ value.to_s # String, blank
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'set'
4
+
5
+ module I18nYamlEditor
6
+ # This is a category
7
+ class Category
8
+ attr_accessor :name, :keys
9
+
10
+ def initialize(attributes = {})
11
+ @name = attributes[:name]
12
+ @keys = Set.new
13
+ end
14
+
15
+ # Adds a given key to this category's list of keys
16
+ # @param key [Key] key to be added to this category's list of keys
17
+ def add_key(key)
18
+ keys.add(key)
19
+ end
20
+
21
+ # Checks and returns if all keys are complete
22
+ # @return [true, false]
23
+ def complete?
24
+ keys.all?(&:complete?)
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Extend Hash with sort_by_key method
4
+ class Hash
5
+ # Sorts entries alphabetically by key
6
+ def sort_by_key(recursive = false, &block)
7
+ keys.sort(&block).each_with_object({}) do |key, seed|
8
+ seed[key] = self[key]
9
+ if recursive && seed[key].is_a?(Hash)
10
+ seed[key] = seed[key].sort_by_key(true, &block)
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module I18nYamlEditor
4
+ # Transformation provides
5
+ module Filter
6
+ # Selects keys from this store according to the given filter options
7
+ def filter_keys(options = {})
8
+ filters = filters(options)
9
+ keys.select { |_, key| filters.all? { |filter| filter.call(key) } }
10
+ end
11
+
12
+ private
13
+
14
+ def filters(options)
15
+ list = []
16
+ list << key_filter(options) if options.key?(:key)
17
+ list << complete_filter(options) if options.key?(:complete)
18
+ list << empty_filter(options) if options.key?(:empty)
19
+ list << text_filter(options) if options.key?(:text)
20
+ list
21
+ end
22
+
23
+ def key_filter(options)
24
+ ->(k) { k.name =~ options[:key] }
25
+ end
26
+
27
+ def complete_filter(options)
28
+ ->(k) { k.complete? == options[:complete] }
29
+ end
30
+
31
+ def empty_filter(options)
32
+ ->(k) { k.empty? == options[:empty] }
33
+ end
34
+
35
+ def text_filter(options)
36
+ ->(k) { k.translations.any? { |t| t.text =~ options[:text] } }
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'set'
4
+
5
+ module I18nYamlEditor
6
+ # This is a key
7
+ class Key
8
+ attr_accessor :name, :translations
9
+
10
+ def initialize(attributes = {})
11
+ @name = attributes[:name]
12
+ @translations = Set.new
13
+ end
14
+
15
+ # Adds a translation to this key's list of translations
16
+ def add_translation(translation)
17
+ translations.add(translation)
18
+ end
19
+
20
+ # This key's category
21
+ def category
22
+ @category ||= name.split('.').first
23
+ end
24
+
25
+ def complete?
26
+ translations.all? { |t| t.text.to_s !~ /\A\s*\z/ } || empty?
27
+ end
28
+
29
+ def empty?
30
+ translations.all? { |t| t.text.to_s =~ /\A\s*\z/ }
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,115 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'set'
4
+ require 'pathname'
5
+
6
+ require 'i18n_yaml_editor/transformation'
7
+ require 'i18n_yaml_editor/filter'
8
+ require 'i18n_yaml_editor/update'
9
+ require 'i18n_yaml_editor/category'
10
+ require 'i18n_yaml_editor/key'
11
+ require 'i18n_yaml_editor/translation'
12
+
13
+ module I18nYamlEditor
14
+ # Raised when translation entries have the same key
15
+ class DuplicateTranslationError < StandardError; end
16
+
17
+ # Store keeps all i18n data
18
+ class Store
19
+ include Transformation
20
+ include Filter
21
+ include Update
22
+
23
+ attr_accessor :categories, :keys, :translations, :locales
24
+
25
+ def initialize
26
+ @categories = {}
27
+ @keys = {}
28
+ @translations = {}
29
+ @locales = Set.new
30
+ end
31
+
32
+ # Adds a given translation to the store
33
+ def add_translation(translation)
34
+ check_duplication! translation
35
+
36
+ translations[translation.name] = translation
37
+
38
+ locales.add(translation.locale)
39
+ key = init_key(translation)
40
+ init_category(key)
41
+ end
42
+
43
+ # Generates a new key with the given translation
44
+ def init_key(translation)
45
+ key = (keys[translation.key] ||= Key.new(name: translation.key))
46
+ key.add_translation(translation)
47
+ key
48
+ end
49
+
50
+ # Generates a new category with the given key
51
+ def init_category(key)
52
+ category = (categories[key.category] ||= Category.new(name: key.category))
53
+ category.add_key(key)
54
+ end
55
+
56
+ # Adds a key to this store
57
+ def add_key(key)
58
+ keys[key.name] = key
59
+ end
60
+
61
+ # Creates all keys for each locale that doesn't have all keys from all
62
+ # other locales
63
+ def create_missing_keys
64
+ keys.each do |_name, key|
65
+ missing_locales = locales - key.translations.map(&:locale)
66
+ missing_locales.each do |locale|
67
+ translation = key.translations.first
68
+ name = "#{locale}.#{key.name}"
69
+ path = translation_path locale, translation
70
+ add_translation(Translation.new(name: name, file: path))
71
+ end
72
+ end
73
+ end
74
+
75
+ # Adds a translation for every entry in a given yaml hash
76
+ def from_yaml(yaml, file = nil)
77
+ translations = flatten_hash(yaml)
78
+ translations.each do |name, text|
79
+ translation = Translation.new(name: name, text: text, file: file)
80
+ add_translation(translation)
81
+ end
82
+ end
83
+
84
+ # Returns a hash with the structure of a i18n
85
+ def to_yaml
86
+ result = {}
87
+ files = translations.values.group_by(&:file)
88
+ files.each do |file, translations|
89
+ file_result = {}
90
+ translations.each do |translation|
91
+ file_result[translation.name] = translation.text
92
+ end
93
+ result[file] = nest_hash(file_result)
94
+ end
95
+ result
96
+ end
97
+
98
+ private
99
+
100
+ def translation_path(locale, translation)
101
+ translation.file.gsub(/(.*)[A-z]{2}(\.yml)\z/, "\\1#{locale}\\2")
102
+ end
103
+
104
+ def check_duplication!(translation)
105
+ existing = translations[translation.name]
106
+ return unless existing
107
+ error_message = error_message(translation, existing)
108
+ raise DuplicateTranslationError, error_message
109
+ end
110
+
111
+ def error_message(translation, existing)
112
+ "#{translation.name} detected in #{translation.file} and #{existing.file}"
113
+ end
114
+ end
115
+ end
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ module I18nYamlEditor
4
+ # Raised when nesting of I18n keys to a Hash fails
5
+ class TransformationError < StandardError; end
6
+
7
+ # Transformation provides
8
+ module Transformation
9
+ # Public: Converts a deep hash to one level by generating new keys
10
+ # by joining the previous key path to the value with a '.'
11
+ #
12
+ # hash - The original Hash.
13
+ # namespace - An optional namespace, default: [].
14
+ # tree - An optional tree to add values to, default: {}.
15
+ #
16
+ # Examples
17
+ # flatten_hash({da: {session: { login: 'Log ind', logout: 'Log ud' }}})
18
+ # # => {"da.session.login"=>"Log ind", "da.session.logout"=>"Log ud"}
19
+ #
20
+ # Returns the generated Hash.
21
+ def flatten_hash(hash, namespace = [], tree = {})
22
+ hash.each do |key, value|
23
+ child_ns = namespace.dup << key
24
+ if value.is_a?(Hash)
25
+ flatten_hash value, child_ns, tree
26
+ else
27
+ tree[child_ns.join('.')] = value
28
+ end
29
+ end
30
+ tree
31
+ end
32
+ module_function :flatten_hash
33
+
34
+ # Public: Converts a flat hash with key path to the value joined
35
+ # with a '.' to a one level Hash, it's the reverse of flatten_hash
36
+ #
37
+ # hash - Hash with keys that represent the path to the value in the new Hash
38
+ #
39
+ # Examples
40
+ # nest_hash({"da.session.login"=>"Log ind", "da.session.logout"=>"Log ud"})
41
+ # # => {"da"=>{"session"=>{"login"=>"Log ind", "logout"=>"Log ud"}}}
42
+ #
43
+ # Returns the generated Hash.
44
+ def nest_hash(hash)
45
+ result = {}
46
+ hash.each do |key, value|
47
+ begin
48
+ nest_key result, key, value
49
+ rescue StandardError
50
+ raise TransformationError,
51
+ "Failed to nest key: #{key.inspect} with #{value.inspect}"
52
+ end
53
+ end
54
+ result
55
+ end
56
+ module_function :nest_hash
57
+
58
+ private
59
+
60
+ # Recursively nests point-separated keys from a string in a hash and
61
+ # assigns the given value to this new hash entry
62
+ def nest_key(result, key, value)
63
+ sub_result = result
64
+ keys = key.split('.')
65
+ keys.each_with_index do |k, idx|
66
+ if keys.size - 1 == idx
67
+ sub_result[k] = value
68
+ else
69
+ sub_result = (sub_result[k] ||= {})
70
+ end
71
+ end
72
+ end
73
+ module_function :nest_key
74
+ end
75
+ end