moirai 0.1.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.
Files changed (45) hide show
  1. checksums.yaml +7 -0
  2. data/.github/CODEOWNERS +1 -0
  3. data/.github/workflows/test.yml +56 -0
  4. data/.gitignore +18 -0
  5. data/Appraisals +15 -0
  6. data/CHANGELOG.md +9 -0
  7. data/Gemfile +17 -0
  8. data/README.md +180 -0
  9. data/Rakefile +17 -0
  10. data/app/assets/config/moirai_manifest.js +2 -0
  11. data/app/assets/images/moirai/.keep +0 -0
  12. data/app/assets/javascripts/moirai/application.js +2 -0
  13. data/app/assets/javascripts/moirai/controllers/moirai_translation_controller.js +27 -0
  14. data/app/assets/javascripts/moirai/stimulus/init.js +5 -0
  15. data/app/assets/stylesheets/moirai/application.css +15 -0
  16. data/app/controllers/concerns/.keep +0 -0
  17. data/app/controllers/moirai/application_controller.rb +21 -0
  18. data/app/controllers/moirai/translation_files_controller.rb +93 -0
  19. data/app/helpers/moirai/application_helper.rb +9 -0
  20. data/app/jobs/moirai/application_job.rb +6 -0
  21. data/app/models/concerns/.keep +0 -0
  22. data/app/models/moirai/application_record.rb +7 -0
  23. data/app/models/moirai/translation.rb +14 -0
  24. data/app/models/moirai/translation_dumper.rb +43 -0
  25. data/app/models/moirai/translation_file_handler.rb +56 -0
  26. data/app/views/layouts/moirai/application.html.erb +26 -0
  27. data/app/views/moirai/translation_files/_form.html.erb +9 -0
  28. data/app/views/moirai/translation_files/index.html.erb +19 -0
  29. data/app/views/moirai/translation_files/show.html.erb +38 -0
  30. data/bin/check +4 -0
  31. data/bin/console +15 -0
  32. data/bin/fastcheck +17 -0
  33. data/bin/rails +16 -0
  34. data/bin/setup +8 -0
  35. data/config/routes.rb +9 -0
  36. data/lib/generators/moirai/install_generator.rb +19 -0
  37. data/lib/generators/moirai/migration_generator.rb +24 -0
  38. data/lib/generators/moirai/templates/migration.rb.erb +11 -0
  39. data/lib/i18n/backend/moirai.rb +65 -0
  40. data/lib/moirai/engine.rb +49 -0
  41. data/lib/moirai/pull_request_creator.rb +83 -0
  42. data/lib/moirai/version.rb +5 -0
  43. data/lib/moirai.rb +10 -0
  44. data/moirai.gemspec +36 -0
  45. metadata +181 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: f735406747923f4a2493a81628e4cd88cf6f54d926fb31448ca5dab4ae70a8b9
4
+ data.tar.gz: 40f69e72824a96f13fe4e67b753a36617a745cdca04c61e691c7cc7877c55b96
5
+ SHA512:
6
+ metadata.gz: 19eeb5b122adcd1f46fbfee0a39440f5d6ef9536bf28f8486f36fed40506d61449793adc6681cd3ce5eb35c10f9b5021928f97494e0c7063a5736fc8c0a2ea90
7
+ data.tar.gz: 9e3d8298eac29bca6008af867b57f8e663d9128c07bccf531c400c2c2889109b15515b676b6bbd0d85144fae119cc087a5ad3bb6cec83564edae67907b73543c
@@ -0,0 +1 @@
1
+ * @renuo/moirai
@@ -0,0 +1,56 @@
1
+ name: Test & lint
2
+ on: [push, pull_request]
3
+
4
+ env:
5
+ RAILS_ENV: test
6
+ PGHOST: localhost
7
+ PGUSER: postgres
8
+
9
+ jobs:
10
+ tests:
11
+ name: Test
12
+
13
+ strategy:
14
+ fail-fast: false
15
+ matrix:
16
+ os: [ubuntu-latest]
17
+ ruby: [3.1, 3.2, 3.3]
18
+
19
+ runs-on: ${{ matrix.os }}
20
+
21
+ steps:
22
+ - name: Checkout code
23
+ uses: actions/checkout@v4
24
+
25
+ - name: Set up Ruby
26
+ uses: ruby/setup-ruby@v1
27
+ with:
28
+ ruby-version: ${{ matrix.ruby }}
29
+ bundler-cache: true
30
+
31
+ - name: Install dependencies
32
+ run: bundle install --jobs 4 --retry 3
33
+
34
+ - name: Run tests
35
+ run: bin/check
36
+
37
+
38
+ lint:
39
+ name: Lint
40
+ runs-on: ubuntu-latest
41
+
42
+ steps:
43
+ - name: Checkout code
44
+ uses: actions/checkout@v2
45
+
46
+ - name: Set up Ruby
47
+ uses: ruby/setup-ruby@v1
48
+ with:
49
+ ruby-version: 3.3
50
+ bundler-cache: true
51
+
52
+ - name: Install dependencies
53
+ run: bundle install --jobs 4 --retry 3
54
+
55
+ - name: Run linters
56
+ run: bin/fastcheck
data/.gitignore ADDED
@@ -0,0 +1,18 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /log/*.log
7
+ /pkg/
8
+ /tmp/
9
+ /test/dummy/db/*.sqlite3
10
+ /test/dummy/db/*.sqlite3-*
11
+ /test/dummy/log/*.log
12
+ /test/dummy/storage/
13
+ /test/dummy/tmp/
14
+
15
+ .env
16
+ /Gemfile.lock
17
+ *.gem
18
+ .idea/
data/Appraisals ADDED
@@ -0,0 +1,15 @@
1
+ appraise "rails_7_2" do
2
+ gem "rails", "~> 7.2"
3
+ end
4
+
5
+ appraise "rails_7_1" do
6
+ gem "rails", "~> 7.1"
7
+ end
8
+
9
+ appraise "rails_7_0" do
10
+ gem "rails", "~> 7.0"
11
+ end
12
+
13
+ appraise "rails_6_1" do
14
+ gem "rails", "~> 6.1"
15
+ end
data/CHANGELOG.md ADDED
@@ -0,0 +1,9 @@
1
+ ## 0.1.0
2
+
3
+ * Gem structure created ([@oliveranthony17][])
4
+
5
+ [@coorasse]: https://github.com/coorasse
6
+
7
+ [@oliveranthony17]: https://github.com/oliveranthony17
8
+
9
+ [@CuddlyBunion341]: https://github.com/CuddlyBunion341
data/Gemfile ADDED
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gemspec
6
+
7
+ group :development, :test do
8
+ gem "puma"
9
+ gem "sqlite3"
10
+ gem "minitest"
11
+ gem "rake"
12
+ gem "standard"
13
+ gem "appraisal"
14
+ gem "better_errors"
15
+ gem "binding_of_caller"
16
+ gem "sprockets-rails"
17
+ end
data/README.md ADDED
@@ -0,0 +1,180 @@
1
+ # 🧵 Moirai
2
+
3
+ <img src="./docs/moirai.png" width="100%" />
4
+
5
+ ### Manage translation strings in real time
6
+
7
+ - Let your non-developer team members finally manage translations (yes, even Karen from marketing...).
8
+ - See those translations live in your app, so you can make sure “Submit” isn’t overlapping the button where “**Do not press this button EVER” should be.
9
+ - Automatically create Pull Requests based on these changes, saving your developers from yet another “small tweak” email request.
10
+
11
+ > Let the world be translated, one typo at a time.
12
+
13
+
14
+ ## Installation
15
+
16
+ Add this line to your application's Gemfile:
17
+
18
+ ```ruby
19
+ gem "moirai"
20
+ ```
21
+
22
+ And then execute:
23
+ ```bash
24
+ bundle
25
+ ```
26
+
27
+ Next, you need to run the generator which will create the necessary files including the database migration, as well as inserting the engine in the `routes.rb` file:
28
+
29
+ ```bash
30
+ bin/rails g moirai:install
31
+ ```
32
+
33
+ Then run:
34
+
35
+ ```bash
36
+ bin/rails db:migrate
37
+ ```
38
+
39
+ ## Usage
40
+
41
+ ### How to change translations
42
+
43
+ If you mounted Moirai under "/moirai", head there and you will find a list of all the files containing texts that can be translated.
44
+ Open a file, change the value of translations, and press ENTER to update the translation and see it immediately changed on the application.
45
+
46
+ ### Inline editing
47
+
48
+ By default, inline editing is disabled. To enable it, set the `moirai=true` query parameter in the URL.
49
+ If you want to only allow specific users to perform inline editing, you can override the `moirai_edit_enabled?` method in your application helper.
50
+
51
+ ```ruby
52
+ module ApplicationHelper
53
+ def moirai_edit_enabled?
54
+ params[:moirai] == "true" || current_user&.admin?
55
+ end
56
+ end
57
+ ```
58
+
59
+ ### Automatic PR creation with Octokit (**optional**)
60
+
61
+ If you would like Moirai to automatically create a pull request on GitHub to keep translations synchronized with the codebase,
62
+ you need to set up [Octokit](https://github.com/octokit/octokit.rb).
63
+ You will also need to create a **Personal Access Token** on GitHub, and configure the access in the appropriate **environment variables** (this is explained below).
64
+
65
+ #### 1. Add Octokit to Your Gemfile
66
+
67
+ First, add Octokit to your project’s Gemfile:
68
+
69
+ ```
70
+ gem 'octokit'
71
+ ```
72
+
73
+ Then run `bundle install`.
74
+
75
+ #### 2. Create a Personal Access Token (PAT) on GitHub
76
+
77
+ You will need a Personal Access Token (PAT) with the `Content - Write` permission to allow Octokit to create branches and pull requests.
78
+
79
+ - Go to GitHub Token Settings.
80
+ - Click Generate New Token.
81
+ - Give your token a name (e.g., “Moirai”).
82
+ - Under Scopes, select:
83
+ - repo (for full control of private repositories, including writing content).
84
+ - content (for read/write access to code, commit statuses, and pull requests).
85
+ - Generate the token and copy it immediately as it will be shown only once.
86
+
87
+ #### 3. Set Up Environment Variables
88
+
89
+ You need to configure the following environment variables in your application:
90
+
91
+ - `MOIRAI_GITHUB_REPO_NAME`: The name of the repository where the pull request will be created.
92
+ - `MOIRAI_GITHUB_ACCESS_TOKEN`: The Personal Access Token (PAT) you created earlier.
93
+
94
+ For example, in your `.env` file:
95
+
96
+ ```env
97
+ MOIRAI_GITHUB_REPO_NAME=your-organization/your-repo
98
+ MOIRAI_GITHUB_ACCESS_TOKEN=your-generated-token
99
+ ```
100
+
101
+ We also support Rails credentials. The environment variables need to be stored in a slightly different way to adhere to convention. For example:
102
+
103
+ ```env
104
+ moirai:
105
+ github_repo_name: your-organization/your-repo
106
+ github_access_token: your-generated-token
107
+ ```
108
+
109
+ #### 4. Triggering the pull request creation
110
+
111
+ Moirai will now be able to use this Personal Access Token to create a pull request on GitHub when a translation is updated.
112
+
113
+ To trigger this, you can press the `Create or update PR` button once you have made your changes.
114
+
115
+ ### Authentication
116
+
117
+ Moirai allows you to use basic HTTP authentication to protect the engine.
118
+ To enable this, you need to set the following environment variables:
119
+
120
+ ```env
121
+ MOIRAI_BASICAUTH_NAME=moirai
122
+ MOIRAI_BASICAUTH_PASSWORD=moirai
123
+ ```
124
+
125
+ > ⚠️ Remember to protect Moirai. You don't want to give everyone the possibility to change strings in the application.
126
+
127
+ If you have authenticated users, you can leverage the Rails Routes protection mechanism to protect the engine.
128
+ See the following example:
129
+
130
+ ```ruby
131
+ authenticated :user, lambda {|u| u.role == "admin"} do
132
+ mount Moirai::Engine => '/moirai', as: 'moirai'
133
+ end
134
+ ```
135
+
136
+ ## Development
137
+
138
+ 1. Check out the repo:
139
+ ```bash
140
+ git clone git@github.com:renuo/moirai.git
141
+ cd moirai
142
+ ```
143
+
144
+ 2. Run the setup script to install dependencies:
145
+ ```bash
146
+ bin/setup
147
+ ```
148
+
149
+ 3. Copy the example environment variables file to create your own `.env` file:
150
+ ```bash
151
+ cp .env.example .env
152
+ ```
153
+
154
+ 4. Set your environment variables using the newly created `.env` file.
155
+
156
+ 5. Run the tests:
157
+ ```bash
158
+ bin/check
159
+ ```
160
+
161
+ 6. To view the engine in a dummy app:
162
+ ```bash
163
+ bin/rails s
164
+ ```
165
+
166
+ ## TODO
167
+
168
+ * Support for HTML
169
+ * Support for interpolation
170
+ * Support for count variants
171
+ * Better inline editing tool
172
+ * Performance of translations lookup
173
+ * Support for translations and strings coming from other gems
174
+
175
+ ## License
176
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
177
+
178
+ ## Copyright
179
+
180
+ Copyright [Renuo AG](https://www.renuo.ch/).
data/Rakefile ADDED
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/setup"
4
+ require "bundler/gem_tasks"
5
+ require "rake/testtask"
6
+
7
+ APP_RAKEFILE = File.expand_path("test/dummy/Rakefile", __dir__)
8
+ load "rails/tasks/engine.rake"
9
+ load "rails/tasks/statistics.rake"
10
+
11
+ Rake::TestTask.new(:test) do |t|
12
+ t.libs << "test"
13
+ t.libs << "lib"
14
+ t.test_files = FileList["test/**/test_*.rb"]
15
+ end
16
+
17
+ task default: :test
@@ -0,0 +1,2 @@
1
+ //= link_directory ../stylesheets/moirai .css
2
+ //= link_directory ../javascripts/moirai .js
File without changes
@@ -0,0 +1,2 @@
1
+ //= require ./stimulus/init
2
+ //= require ./controllers/moirai_translation_controller
@@ -0,0 +1,27 @@
1
+ Stimulus.register(
2
+ "moirai-translation",
3
+ class extends Controller {
4
+ click(event) {
5
+ event.preventDefault()
6
+ }
7
+
8
+ submit(event) {
9
+ const {filePath, key} = event.target.dataset
10
+
11
+ const csrfToken = document.querySelector('meta[name="csrf-token"]').content
12
+
13
+ const formObject = new FormData()
14
+ formObject.append('translation[key]', key)
15
+ formObject.append('translation[file_path]', filePath)
16
+ formObject.append('translation[value]', event.target.innerText)
17
+
18
+ fetch(`/moirai/translation_files`, {
19
+ method: 'POST',
20
+ headers: {
21
+ 'X-CSRF-Token': csrfToken
22
+ },
23
+ body: formObject
24
+ })
25
+ }
26
+ }
27
+ );
@@ -0,0 +1,5 @@
1
+ import {
2
+ Application,
3
+ Controller,
4
+ } from "https://unpkg.com/@hotwired/stimulus/dist/stimulus.js";
5
+ window.Stimulus = Application.start();
@@ -0,0 +1,15 @@
1
+ /*
2
+ * This is a manifest file that'll be compiled into application.css, which will include all the files
3
+ * listed below.
4
+ *
5
+ * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets,
6
+ * or any plugin's vendor/assets/stylesheets directory can be referenced here using a relative path.
7
+ *
8
+ * You're free to add application-wide styles to this file and they'll appear at the bottom of the
9
+ * compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS
10
+ * files in this directory. Styles in this file should be added after the last require_* statement.
11
+ * It is generally better to create a new file per style scope.
12
+ *
13
+ *= require_tree .
14
+ *= require_self
15
+ */
File without changes
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Moirai
4
+ class ApplicationController < ActionController::Base
5
+ before_action :authenticate, if: :basic_auth_present?
6
+
7
+ def authenticate
8
+ if basic_auth_present?
9
+ authenticate_or_request_with_http_basic do |name, password|
10
+ name == ENV["MOIRAI_BASICAUTH_NAME"] && password == ENV["MOIRAI_BASICAUTH_PASSWORD"]
11
+ end
12
+ end
13
+ end
14
+
15
+ private
16
+
17
+ def basic_auth_present?
18
+ ENV["MOIRAI_BASICAUTH_NAME"].present? && ENV["MOIRAI_BASICAUTH_PASSWORD"].present?
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,93 @@
1
+ module Moirai
2
+ class TranslationFilesController < ApplicationController
3
+ before_action :load_file_handler, only: [:index, :show, :create_or_update]
4
+ before_action :set_translation_file, only: [:show]
5
+
6
+ def index
7
+ @files = @file_handler.file_paths.map do |path|
8
+ {
9
+ id: Digest::SHA256.hexdigest(path),
10
+ name: File.basename(path),
11
+ path: path
12
+ }
13
+ end
14
+ end
15
+
16
+ def show
17
+ @translation_keys = @file_handler.parse_file(@decoded_path)
18
+ end
19
+
20
+ def create_or_update
21
+ if (translation = Translation.find_by(file_path: translation_params[:file_path],
22
+ key: translation_params[:key],
23
+ locale: @file_handler.get_first_key(translation_params[:file_path])))
24
+ handle_update(translation)
25
+ else
26
+ handle_create
27
+ end
28
+ end
29
+
30
+ def open_pr
31
+ flash.notice = "I created an amazing PR"
32
+ changes = Moirai::TranslationDumper.new.call
33
+ Moirai::PullRequestCreator.new.create_pull_request(changes)
34
+ redirect_back_or_to(root_path)
35
+ end
36
+
37
+ private
38
+
39
+ def handle_update(translation)
40
+ translation_from_file = @file_handler.parse_file(translation_params[:file_path])
41
+ if translation_from_file[translation.key] == translation_params[:value] || translation_params[:value].blank?
42
+ translation.destroy
43
+ flash.notice = "Translation #{translation.key} was successfully deleted."
44
+ redirect_to_translation_file(translation.file_path)
45
+ return
46
+ end
47
+
48
+ if translation.update(value: translation_params[:value])
49
+ flash.notice = "Translation #{translation.key} was successfully updated."
50
+ else
51
+ flash.alert = translation.errors.full_messages.join(", ")
52
+ end
53
+
54
+ redirect_to_translation_file(translation.file_path)
55
+ end
56
+
57
+ def handle_create
58
+ translation_from_file = @file_handler.parse_file(translation_params[:file_path])
59
+ if translation_from_file[translation_params[:key]] == translation_params[:value]
60
+ flash.alert = "Translation #{translation_params[:key]} already exists."
61
+ redirect_to_translation_file(translation_params[:file_path])
62
+ return
63
+ end
64
+
65
+ translation = Translation.new(translation_params)
66
+ translation.locale = @file_handler.get_first_key(translation_params[:file_path])
67
+ if translation.save
68
+ flash.notice = "Translation #{translation.key} was successfully created."
69
+ else
70
+ flash.alert = translation.errors.full_messages.join(", ")
71
+ end
72
+
73
+ redirect_to_translation_file(translation.file_path)
74
+ end
75
+
76
+ def redirect_to_translation_file(file_path)
77
+ redirect_back_or_to moirai_translation_file_path(Digest::SHA256.hexdigest(file_path))
78
+ end
79
+
80
+ def set_translation_file
81
+ @file_path = @file_handler.file_hashes[params[:id]]
82
+ @decoded_path = CGI.unescape(@file_path)
83
+ end
84
+
85
+ def translation_params
86
+ params.require(:translation).permit(:key, :locale, :value, :file_path)
87
+ end
88
+
89
+ def load_file_handler
90
+ @file_handler = Moirai::TranslationFileHandler.new
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Moirai
4
+ module ApplicationHelper
5
+ def shorten_path(path)
6
+ path.gsub(Rails.root.to_s, ".")
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Moirai
4
+ class ApplicationJob < ActiveJob::Base
5
+ end
6
+ end
File without changes
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Moirai
4
+ class ApplicationRecord < ActiveRecord::Base
5
+ self.abstract_class = true
6
+ end
7
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Moirai
4
+ class Translation < Moirai::ApplicationRecord
5
+ validates_presence_of :key, :locale, :file_path
6
+ validate :file_path_must_exist
7
+
8
+ private
9
+
10
+ def file_path_must_exist
11
+ errors.add(:file_path, "must exist") unless file_path && File.exist?(file_path)
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,43 @@
1
+ module Moirai
2
+ class TranslationDumper
3
+ def call
4
+ project_root = Rails.root.to_s
5
+ Moirai::Translation.pluck(:file_path).uniq.map do |file_path|
6
+ absolute_file_path = File.expand_path(file_path, project_root)
7
+ next unless absolute_file_path.start_with?(project_root)
8
+
9
+ updated_file_contents = get_updated_file_contents(file_path)
10
+ {
11
+ file_path: file_path.sub(project_root, "."),
12
+ content: updated_file_contents
13
+ }
14
+ end.compact
15
+ end
16
+
17
+ private
18
+
19
+ def get_updated_file_contents(file_path)
20
+ translations = Moirai::Translation.where(file_path: file_path)
21
+
22
+ yaml = YAML.load_file(file_path)
23
+
24
+ translations.each do |translation|
25
+ keys = [translation.locale] + translation.key.split(".")
26
+
27
+ node = yaml
28
+
29
+ (0...keys.size).each do |i|
30
+ key = keys[i]
31
+ if i == keys.size - 1
32
+ node[key] = translation.value
33
+ else
34
+ node[key] ||= {}
35
+ node = node[key]
36
+ end
37
+ end
38
+ end
39
+
40
+ yaml.to_yaml
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Moirai
4
+ class TranslationFileHandler
5
+ attr_reader :file_paths, :file_hashes
6
+
7
+ def initialize
8
+ load_file_paths
9
+ generate_file_hashes
10
+ end
11
+
12
+ def parse_file(path)
13
+ yaml_content = YAML.load_file(path)
14
+ root_key = yaml_content.keys.first
15
+ flatten_hash(yaml_content[root_key])
16
+ end
17
+
18
+ def get_first_key(file_path)
19
+ yaml_content = YAML.load_file(file_path)
20
+ yaml_content.keys.first
21
+ end
22
+
23
+ private
24
+
25
+ def load_file_paths
26
+ i18n_file_paths = I18n.load_path
27
+ @file_paths = i18n_file_paths.select { |path| path.starts_with?(Rails.root.to_s) && path.end_with?(".yml", ".yaml") }
28
+ end
29
+
30
+ def generate_file_hashes
31
+ @file_hashes = @file_paths.map { |path| [Digest::SHA256.hexdigest(path), path] }.to_h
32
+ end
33
+
34
+ def flatten_hash(hash, parent_key = "", result = {})
35
+ hash.each do |key, value|
36
+ new_key = parent_key.empty? ? key.to_s : "#{parent_key}.#{key}"
37
+ case value
38
+ when Hash
39
+ flatten_hash(value, new_key, result)
40
+ when Array
41
+ value.each_with_index do |item, index|
42
+ array_key = "#{new_key}.#{index}"
43
+ if item.is_a?(Hash)
44
+ flatten_hash(item, array_key, result)
45
+ else
46
+ result[array_key] = item
47
+ end
48
+ end
49
+ else
50
+ result[new_key] = value
51
+ end
52
+ end
53
+ result
54
+ end
55
+ end
56
+ end