i18n_yaml_editor 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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