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