moirai 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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