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.
- checksums.yaml +7 -0
- data/.github/CODEOWNERS +1 -0
- data/.github/workflows/test.yml +56 -0
- data/.gitignore +18 -0
- data/Appraisals +15 -0
- data/CHANGELOG.md +9 -0
- data/Gemfile +17 -0
- data/README.md +180 -0
- data/Rakefile +17 -0
- data/app/assets/config/moirai_manifest.js +2 -0
- data/app/assets/images/moirai/.keep +0 -0
- data/app/assets/javascripts/moirai/application.js +2 -0
- data/app/assets/javascripts/moirai/controllers/moirai_translation_controller.js +27 -0
- data/app/assets/javascripts/moirai/stimulus/init.js +5 -0
- data/app/assets/stylesheets/moirai/application.css +15 -0
- data/app/controllers/concerns/.keep +0 -0
- data/app/controllers/moirai/application_controller.rb +21 -0
- data/app/controllers/moirai/translation_files_controller.rb +93 -0
- data/app/helpers/moirai/application_helper.rb +9 -0
- data/app/jobs/moirai/application_job.rb +6 -0
- data/app/models/concerns/.keep +0 -0
- data/app/models/moirai/application_record.rb +7 -0
- data/app/models/moirai/translation.rb +14 -0
- data/app/models/moirai/translation_dumper.rb +43 -0
- data/app/models/moirai/translation_file_handler.rb +56 -0
- data/app/views/layouts/moirai/application.html.erb +26 -0
- data/app/views/moirai/translation_files/_form.html.erb +9 -0
- data/app/views/moirai/translation_files/index.html.erb +19 -0
- data/app/views/moirai/translation_files/show.html.erb +38 -0
- data/bin/check +4 -0
- data/bin/console +15 -0
- data/bin/fastcheck +17 -0
- data/bin/rails +16 -0
- data/bin/setup +8 -0
- data/config/routes.rb +9 -0
- data/lib/generators/moirai/install_generator.rb +19 -0
- data/lib/generators/moirai/migration_generator.rb +24 -0
- data/lib/generators/moirai/templates/migration.rb.erb +11 -0
- data/lib/i18n/backend/moirai.rb +65 -0
- data/lib/moirai/engine.rb +49 -0
- data/lib/moirai/pull_request_creator.rb +83 -0
- data/lib/moirai/version.rb +5 -0
- data/lib/moirai.rb +10 -0
- data/moirai.gemspec +36 -0
- 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
|
data/.github/CODEOWNERS
ADDED
@@ -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
data/CHANGELOG.md
ADDED
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
|
File without changes
|
@@ -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,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
|
File without changes
|
@@ -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
|