moirai 0.1.0 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/workflows/test.yml +15 -6
- data/CHANGELOG.md +21 -0
- data/Gemfile +12 -1
- data/README.md +63 -27
- data/app/assets/config/moirai_manifest.js +0 -1
- data/app/assets/javascripts/moirai_translation_controller.js +41 -0
- data/app/assets/stylesheets/moirai/application.css +2 -0
- data/app/assets/stylesheets/translation_files.css +22 -0
- data/app/controllers/moirai/translation_files_controller.rb +66 -18
- data/app/controllers/moirai/translations_controller.rb +7 -0
- data/app/models/moirai/change.rb +15 -0
- data/app/models/moirai/key_finder.rb +55 -0
- data/app/models/moirai/translation.rb +9 -5
- data/app/models/moirai/translation_dumper.rb +40 -13
- data/app/views/layouts/moirai/application.html.erb +28 -1
- data/app/views/moirai/translation_files/_form.html.erb +3 -3
- data/app/views/moirai/translation_files/index.html.erb +1 -1
- data/app/views/moirai/translation_files/show.html.erb +11 -5
- data/app/views/moirai/translations/index.html.erb +7 -0
- data/bin/check +3 -1
- data/config/routes.rb +2 -1
- data/lib/generators/moirai/install_generator.rb +27 -2
- data/lib/generators/moirai/migration_generator.rb +5 -1
- data/lib/generators/moirai/templates/make_moirai_translations_file_path_not_required.rb.erb +5 -0
- data/lib/i18n/backend/moirai.rb +1 -53
- data/lib/i18n/extensions/i18n.rb +13 -0
- data/lib/moirai/engine.rb +8 -6
- data/lib/moirai/pull_request_creator.rb +42 -33
- data/lib/moirai/version.rb +1 -1
- data/lib/moirai.rb +2 -1
- data/moirai.gemspec +3 -6
- metadata +12 -77
- data/app/assets/javascripts/moirai/application.js +0 -2
- data/app/assets/javascripts/moirai/controllers/moirai_translation_controller.js +0 -27
- data/app/assets/javascripts/moirai/stimulus/init.js +0 -5
- /data/lib/generators/moirai/templates/{migration.rb.erb → create_moirai_translations.rb.erb} +0 -0
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: e9d7f98e48e08b3e5208041fbba3344f5b280492754b7943e159d0e90e71d075
|
4
|
+
data.tar.gz: d4aa78c7ae9f0c27f18c3e760def299c5d8faf054b06423e300b2c095976ef0c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: cb39e17d721373254b27086f4cf351c6145a3aa921809b8de0921ca82c661d53e0a1fb38da524c4fe012c2ecfcde7ffa2ec1029432e88d0cc43cf2f067c55341
|
7
|
+
data.tar.gz: 601b7c5353bbc71cd094223df88b74dc9f82d07657f306f8402160bc47f4d056940a28db55b8b88f4fd2275c23e66379e2778e2856f0d93a57defd7b706dc68c
|
data/.github/workflows/test.yml
CHANGED
@@ -1,11 +1,12 @@
|
|
1
1
|
name: Test & lint
|
2
|
-
on: [push
|
2
|
+
on: [push]
|
3
3
|
|
4
4
|
env:
|
5
5
|
RAILS_ENV: test
|
6
6
|
PGHOST: localhost
|
7
7
|
PGUSER: postgres
|
8
|
-
|
8
|
+
MOIRAI_GITHUB_REPO_NAME: ${{ secrets.MOIRAI_GITHUB_REPO_NAME }}
|
9
|
+
MOIRAI_GITHUB_ACCESS_TOKEN: ${{ secrets.MOIRAI_GITHUB_ACCESS_TOKEN }}
|
9
10
|
jobs:
|
10
11
|
tests:
|
11
12
|
name: Test
|
@@ -14,7 +15,7 @@ jobs:
|
|
14
15
|
fail-fast: false
|
15
16
|
matrix:
|
16
17
|
os: [ubuntu-latest]
|
17
|
-
ruby: [3.
|
18
|
+
ruby: [3.3]
|
18
19
|
|
19
20
|
runs-on: ${{ matrix.os }}
|
20
21
|
|
@@ -32,9 +33,17 @@ jobs:
|
|
32
33
|
run: bundle install --jobs 4 --retry 3
|
33
34
|
|
34
35
|
- name: Run tests
|
35
|
-
run:
|
36
|
-
|
37
|
-
|
36
|
+
run: bundle exec rails test
|
37
|
+
|
38
|
+
- name: Run system tests
|
39
|
+
run: bundle exec rails test:system
|
40
|
+
|
41
|
+
- name: Archive logs
|
42
|
+
if: always()
|
43
|
+
uses: actions/upload-artifact@v4
|
44
|
+
with:
|
45
|
+
name: test.log
|
46
|
+
path: test/dummy/log/test.log
|
38
47
|
lint:
|
39
48
|
name: Lint
|
40
49
|
runs-on: ubuntu-latest
|
data/CHANGELOG.md
CHANGED
@@ -1,6 +1,27 @@
|
|
1
|
+
## 0.3.0
|
2
|
+
|
3
|
+
* Added a method `I18n.translate_without_moirai` ([@oliveranthony17][])
|
4
|
+
* Simplified stimulus setup ([@coorasse][])
|
5
|
+
* Fixed some setup issues in test environments ([@oliveranthony17][])
|
6
|
+
* Show original translation when deleting the whole inline editing content. ([@CuddlyBunion341][])
|
7
|
+
|
8
|
+
## 0.2.0
|
9
|
+
|
10
|
+
* Support for strings coming from gems ([@coorasse][])
|
11
|
+
* Support for new strings (not yet translated) ([@coorasse][])
|
12
|
+
|
13
|
+
## 0.1.1
|
14
|
+
|
15
|
+
* Review Stimulus controller ([@coorasse][])
|
16
|
+
|
1
17
|
## 0.1.0
|
2
18
|
|
3
19
|
* Gem structure created ([@oliveranthony17][])
|
20
|
+
* Database tables created ([@oliveranthony17][])
|
21
|
+
* Pull request creation ([@oliveranthony17][])
|
22
|
+
* Dummy app for tests ([@coorasse][])
|
23
|
+
* CRUD for translations ([@CuddlyBunion341][])
|
24
|
+
* Inline editing ([@CuddlyBunion341][])
|
4
25
|
|
5
26
|
[@coorasse]: https://github.com/coorasse
|
6
27
|
|
data/Gemfile
CHANGED
@@ -4,14 +4,25 @@ source "https://rubygems.org"
|
|
4
4
|
|
5
5
|
gemspec
|
6
6
|
|
7
|
+
gem "rails", "~> 7.2.0"
|
8
|
+
gem "octokit", ">= 4.0"
|
9
|
+
gem "importmap-rails"
|
10
|
+
gem "stimulus-rails"
|
11
|
+
|
7
12
|
group :development, :test do
|
8
13
|
gem "puma"
|
9
14
|
gem "sqlite3"
|
10
|
-
gem "minitest"
|
11
15
|
gem "rake"
|
16
|
+
gem "dotenv"
|
17
|
+
gem "minitest"
|
12
18
|
gem "standard"
|
13
19
|
gem "appraisal"
|
14
20
|
gem "better_errors"
|
15
21
|
gem "binding_of_caller"
|
16
22
|
gem "sprockets-rails"
|
17
23
|
end
|
24
|
+
|
25
|
+
group :test do
|
26
|
+
gem "capybara"
|
27
|
+
gem "selenium-webdriver"
|
28
|
+
end
|
data/README.md
CHANGED
@@ -5,12 +5,13 @@
|
|
5
5
|
### Manage translation strings in real time
|
6
6
|
|
7
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
|
9
|
-
|
8
|
+
- See those translations live in your app, so you can make sure “Submit” isn’t overlapping the button where “**Do not
|
9
|
+
press this button EVER” should be.
|
10
|
+
- Automatically create Pull Requests based on these changes, saving your developers from yet another “small tweak” email
|
11
|
+
request.
|
10
12
|
|
11
13
|
> Let the world be translated, one typo at a time.
|
12
14
|
|
13
|
-
|
14
15
|
## Installation
|
15
16
|
|
16
17
|
Add this line to your application's Gemfile:
|
@@ -20,11 +21,13 @@ gem "moirai"
|
|
20
21
|
```
|
21
22
|
|
22
23
|
And then execute:
|
24
|
+
|
23
25
|
```bash
|
24
26
|
bundle
|
25
27
|
```
|
26
28
|
|
27
|
-
Next, you need to run the generator which will create the necessary files including the database migration,
|
29
|
+
Next, you need to run the generator which will create the necessary files including the database migration,
|
30
|
+
as well as inserting the engine in the `routes.rb` file, and importing the necessary javascript files:
|
28
31
|
|
29
32
|
```bash
|
30
33
|
bin/rails g moirai:install
|
@@ -40,27 +43,51 @@ bin/rails db:migrate
|
|
40
43
|
|
41
44
|
### How to change translations
|
42
45
|
|
43
|
-
If you mounted Moirai under "/moirai", head there and you will find a list of all the files containing texts that can be
|
44
|
-
|
46
|
+
If you mounted Moirai under "/moirai", head there and you will find a list of all the files containing texts that can be
|
47
|
+
translated.
|
48
|
+
Open a file, change the value of translations, and press ENTER to update the translation and see it immediately changed
|
49
|
+
on the application.
|
45
50
|
|
46
51
|
### Inline editing
|
47
52
|
|
48
53
|
By default, inline editing is disabled. To enable it, set the `moirai=true` query parameter in the URL.
|
49
|
-
|
54
|
+
|
55
|
+
If you want to only allow specific users to perform inline editing, you can override the `moirai_edit_enabled?` method
|
56
|
+
in your application helper.
|
50
57
|
|
51
58
|
```ruby
|
59
|
+
|
52
60
|
module ApplicationHelper
|
53
61
|
def moirai_edit_enabled?
|
54
|
-
params[:moirai] == "true"
|
62
|
+
params[:moirai] == "true" && current_user&.admin?
|
55
63
|
end
|
56
64
|
end
|
57
65
|
```
|
58
66
|
|
67
|
+
You also need to have the moirai_translations_controller.js Stimulus Controller initialized.
|
68
|
+
|
69
|
+
#### Importmap
|
70
|
+
|
71
|
+
The command `bin/rails g moirai:install` should have already pinned the necessary controller for you in importmap.rb, so
|
72
|
+
no further steps are needed.
|
73
|
+
|
74
|
+
#### jsbulding
|
75
|
+
|
76
|
+
The command `bin/rails g moirai:install` should have already copied the necessary controller for you in
|
77
|
+
`app/javascripts/controllers`, so no further steps are needed.
|
78
|
+
|
79
|
+
#### More?
|
80
|
+
|
81
|
+
If you’re unsure about all the possible configuration options, you can simply copy and paste the stimulus controller
|
82
|
+
into your app as a fallback.
|
83
|
+
|
59
84
|
### Automatic PR creation with Octokit (**optional**)
|
60
85
|
|
61
|
-
If you would like Moirai to automatically create a pull request on GitHub to keep translations synchronized with the
|
86
|
+
If you would like Moirai to automatically create a pull request on GitHub to keep translations synchronized with the
|
87
|
+
codebase,
|
62
88
|
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
|
89
|
+
You will also need to create a **Personal Access Token** on GitHub, and configure the access in the appropriate *
|
90
|
+
*environment variables** (this is explained below).
|
64
91
|
|
65
92
|
#### 1. Add Octokit to Your Gemfile
|
66
93
|
|
@@ -74,22 +101,23 @@ Then run `bundle install`.
|
|
74
101
|
|
75
102
|
#### 2. Create a Personal Access Token (PAT) on GitHub
|
76
103
|
|
77
|
-
You will need a Personal Access Token (PAT) with the `Content - Write` permission to allow Octokit to create branches
|
104
|
+
You will need a Personal Access Token (PAT) with the `Content - Write` permission to allow Octokit to create branches
|
105
|
+
and pull requests.
|
78
106
|
|
79
|
-
-
|
80
|
-
-
|
81
|
-
-
|
82
|
-
-
|
83
|
-
|
84
|
-
|
85
|
-
-
|
107
|
+
- Go to GitHub Token Settings.
|
108
|
+
- Click Generate New Token.
|
109
|
+
- Give your token a name (e.g., “Moirai”).
|
110
|
+
- Under Scopes, select:
|
111
|
+
- repo (for full control of private repositories, including writing content).
|
112
|
+
- content (for read/write access to code, commit statuses, and pull requests).
|
113
|
+
- Generate the token and copy it immediately as it will be shown only once.
|
86
114
|
|
87
115
|
#### 3. Set Up Environment Variables
|
88
116
|
|
89
117
|
You need to configure the following environment variables in your application:
|
90
118
|
|
91
|
-
-
|
92
|
-
-
|
119
|
+
- `MOIRAI_GITHUB_REPO_NAME`: The name of the repository where the pull request will be created.
|
120
|
+
- `MOIRAI_GITHUB_ACCESS_TOKEN`: The Personal Access Token (PAT) you created earlier.
|
93
121
|
|
94
122
|
For example, in your `.env` file:
|
95
123
|
|
@@ -98,7 +126,8 @@ MOIRAI_GITHUB_REPO_NAME=your-organization/your-repo
|
|
98
126
|
MOIRAI_GITHUB_ACCESS_TOKEN=your-generated-token
|
99
127
|
```
|
100
128
|
|
101
|
-
We also support Rails credentials. The environment variables need to be stored in a slightly different way to adhere to
|
129
|
+
We also support Rails credentials. The environment variables need to be stored in a slightly different way to adhere to
|
130
|
+
convention. For example:
|
102
131
|
|
103
132
|
```env
|
104
133
|
moirai:
|
@@ -108,13 +137,14 @@ moirai:
|
|
108
137
|
|
109
138
|
#### 4. Triggering the pull request creation
|
110
139
|
|
111
|
-
Moirai will now be able to use this Personal Access Token to create a pull request on GitHub when a translation is
|
140
|
+
Moirai will now be able to use this Personal Access Token to create a pull request on GitHub when a translation is
|
141
|
+
updated.
|
112
142
|
|
113
143
|
To trigger this, you can press the `Create or update PR` button once you have made your changes.
|
114
144
|
|
115
145
|
### Authentication
|
116
146
|
|
117
|
-
Moirai allows you to use basic HTTP authentication to protect the engine.
|
147
|
+
Moirai allows you to use basic HTTP authentication to protect the engine.
|
118
148
|
To enable this, you need to set the following environment variables:
|
119
149
|
|
120
150
|
```env
|
@@ -124,11 +154,11 @@ MOIRAI_BASICAUTH_PASSWORD=moirai
|
|
124
154
|
|
125
155
|
> ⚠️ Remember to protect Moirai. You don't want to give everyone the possibility to change strings in the application.
|
126
156
|
|
127
|
-
If you have authenticated users, you can leverage the Rails Routes protection mechanism to protect the engine.
|
157
|
+
If you have authenticated users, you can leverage the Rails Routes protection mechanism to protect the engine.
|
128
158
|
See the following example:
|
129
159
|
|
130
160
|
```ruby
|
131
|
-
authenticated :user, lambda {|u| u.role == "admin"} do
|
161
|
+
authenticated :user, lambda { |u| u.role == "admin" } do
|
132
162
|
mount Moirai::Engine => '/moirai', as: 'moirai'
|
133
163
|
end
|
134
164
|
```
|
@@ -153,6 +183,12 @@ end
|
|
153
183
|
|
154
184
|
4. Set your environment variables using the newly created `.env` file.
|
155
185
|
|
186
|
+
You will need a repository to test against and a token. Generate a new Fine-GRained Personal access token and give the
|
187
|
+
necessary permissions to your repository.
|
188
|
+
See the image below as an example:
|
189
|
+
|
190
|
+
![](docs/github_settings.png)
|
191
|
+
|
156
192
|
5. Run the tests:
|
157
193
|
```bash
|
158
194
|
bin/check
|
@@ -169,10 +205,10 @@ end
|
|
169
205
|
* Support for interpolation
|
170
206
|
* Support for count variants
|
171
207
|
* Better inline editing tool
|
172
|
-
*
|
173
|
-
* Support for translations and strings coming from other gems
|
208
|
+
* Support for fallbacks: it should detect when a fallback string is in use and prevent attempts to override its value.
|
174
209
|
|
175
210
|
## License
|
211
|
+
|
176
212
|
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
177
213
|
|
178
214
|
## Copyright
|
@@ -0,0 +1,41 @@
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
2
|
+
|
3
|
+
export default class MoiraiTranslationController extends Controller {
|
4
|
+
static values = {
|
5
|
+
key: String,
|
6
|
+
locale: String
|
7
|
+
}
|
8
|
+
|
9
|
+
click(event) {
|
10
|
+
event.preventDefault()
|
11
|
+
}
|
12
|
+
|
13
|
+
submit(event) {
|
14
|
+
const csrfToken = document.querySelector('meta[name="csrf-token"]').content
|
15
|
+
|
16
|
+
fetch('/moirai/translation_files', {
|
17
|
+
method: 'POST',
|
18
|
+
headers: {
|
19
|
+
'X-CSRF-Token': csrfToken,
|
20
|
+
'Content-Type': 'application/json',
|
21
|
+
'Accept': 'application/json'
|
22
|
+
},
|
23
|
+
body: JSON.stringify({
|
24
|
+
translation: {
|
25
|
+
key: this.keyValue,
|
26
|
+
locale: this.localeValue,
|
27
|
+
value: event.target.innerText
|
28
|
+
}
|
29
|
+
})
|
30
|
+
})
|
31
|
+
.then(response => response.json())
|
32
|
+
.then(data => {
|
33
|
+
if (data?.fallback_translation) {
|
34
|
+
event.target.innerText = data.fallback_translation
|
35
|
+
}
|
36
|
+
})
|
37
|
+
.catch(error => {
|
38
|
+
console.error('Error:', error);
|
39
|
+
});
|
40
|
+
}
|
41
|
+
}
|
@@ -0,0 +1,22 @@
|
|
1
|
+
td {
|
2
|
+
height: 100px;
|
3
|
+
width: 200px;
|
4
|
+
vertical-align: top;
|
5
|
+
}
|
6
|
+
|
7
|
+
form {
|
8
|
+
height: 100%;
|
9
|
+
display: flex;
|
10
|
+
align-items: stretch;
|
11
|
+
}
|
12
|
+
|
13
|
+
textarea.translation-textarea {
|
14
|
+
width: 100%;
|
15
|
+
height: auto;
|
16
|
+
resize: vertical;
|
17
|
+
min-height: 3em;
|
18
|
+
overflow: hidden;
|
19
|
+
margin-bottom: 0;
|
20
|
+
}
|
21
|
+
|
22
|
+
/*# TODO: this isn't coming through */
|
@@ -14,13 +14,13 @@ module Moirai
|
|
14
14
|
end
|
15
15
|
|
16
16
|
def show
|
17
|
-
@translation_keys = @file_handler.parse_file(@
|
17
|
+
@translation_keys = @file_handler.parse_file(@file_path)
|
18
|
+
@locale = @file_handler.get_first_key(@file_path)
|
19
|
+
@translations = Moirai::Translation.by_file_path(@file_path)
|
18
20
|
end
|
19
21
|
|
20
22
|
def create_or_update
|
21
|
-
if (translation = Translation.find_by(
|
22
|
-
key: translation_params[:key],
|
23
|
-
locale: @file_handler.get_first_key(translation_params[:file_path])))
|
23
|
+
if (translation = Translation.find_by(key: translation_params[:key], locale: translation_params[:locale]))
|
24
24
|
handle_update(translation)
|
25
25
|
else
|
26
26
|
handle_create
|
@@ -28,7 +28,7 @@ module Moirai
|
|
28
28
|
end
|
29
29
|
|
30
30
|
def open_pr
|
31
|
-
flash.notice = "I created an amazing
|
31
|
+
flash.notice = "I created an amazing Pull Request"
|
32
32
|
changes = Moirai::TranslationDumper.new.call
|
33
33
|
Moirai::PullRequestCreator.new.create_pull_request(changes)
|
34
34
|
redirect_back_or_to(root_path)
|
@@ -37,11 +37,19 @@ module Moirai
|
|
37
37
|
private
|
38
38
|
|
39
39
|
def handle_update(translation)
|
40
|
-
|
41
|
-
if translation_from_file[translation.key] == translation_params[:value] || translation_params[:value].blank?
|
40
|
+
if translation_params[:value].blank? || translation_same_as_current?
|
42
41
|
translation.destroy
|
43
|
-
|
44
|
-
|
42
|
+
respond_to do |format|
|
43
|
+
format.json do
|
44
|
+
render json: {
|
45
|
+
fallback_translation: get_fallback_translation
|
46
|
+
}
|
47
|
+
end
|
48
|
+
format.html do
|
49
|
+
flash.notice = "Translation #{translation.key} was successfully deleted."
|
50
|
+
redirect_to_translation_file(translation.file_path)
|
51
|
+
end
|
52
|
+
end
|
45
53
|
return
|
46
54
|
end
|
47
55
|
|
@@ -51,26 +59,41 @@ module Moirai
|
|
51
59
|
flash.alert = translation.errors.full_messages.join(", ")
|
52
60
|
end
|
53
61
|
|
54
|
-
|
62
|
+
success_response(translation)
|
55
63
|
end
|
56
64
|
|
57
65
|
def handle_create
|
58
|
-
|
59
|
-
if translation_from_file[translation_params[:key]] == translation_params[:value]
|
66
|
+
if translation_same_as_current?
|
60
67
|
flash.alert = "Translation #{translation_params[:key]} already exists."
|
61
|
-
|
68
|
+
redirect_back_or_to moirai_translation_files_path, status: :unprocessable_entity
|
62
69
|
return
|
63
70
|
end
|
64
71
|
|
72
|
+
if translation_params[:value].blank? && request.format.json?
|
73
|
+
return render json: {fallback_translation: get_fallback_translation}
|
74
|
+
end
|
75
|
+
|
65
76
|
translation = Translation.new(translation_params)
|
66
|
-
|
77
|
+
|
67
78
|
if translation.save
|
68
79
|
flash.notice = "Translation #{translation.key} was successfully created."
|
80
|
+
success_response(translation)
|
69
81
|
else
|
70
82
|
flash.alert = translation.errors.full_messages.join(", ")
|
83
|
+
redirect_back_or_to moirai_translation_files_path, status: :unprocessable_entity
|
71
84
|
end
|
85
|
+
end
|
72
86
|
|
73
|
-
|
87
|
+
def success_response(translation)
|
88
|
+
respond_to do |format|
|
89
|
+
format.json do
|
90
|
+
flash.discard
|
91
|
+
render json: {}
|
92
|
+
end
|
93
|
+
format.all do
|
94
|
+
redirect_to_translation_file(translation.file_path)
|
95
|
+
end
|
96
|
+
end
|
74
97
|
end
|
75
98
|
|
76
99
|
def redirect_to_translation_file(file_path)
|
@@ -78,16 +101,41 @@ module Moirai
|
|
78
101
|
end
|
79
102
|
|
80
103
|
def set_translation_file
|
81
|
-
@file_path = @file_handler.file_hashes[params[:
|
82
|
-
|
104
|
+
@file_path = @file_handler.file_hashes[params[:hashed_file_path]]
|
105
|
+
if @file_path.nil?
|
106
|
+
flash.alert = "File not found"
|
107
|
+
redirect_to moirai_translation_files_path, status: :not_found
|
108
|
+
end
|
83
109
|
end
|
84
110
|
|
85
111
|
def translation_params
|
86
|
-
params.require(:translation).permit(:key, :locale, :value
|
112
|
+
params.require(:translation).permit(:key, :locale, :value)
|
87
113
|
end
|
88
114
|
|
89
115
|
def load_file_handler
|
90
116
|
@file_handler = Moirai::TranslationFileHandler.new
|
91
117
|
end
|
118
|
+
|
119
|
+
# TODO: to resolve the last point of the TODOs we could look at the current translation (without moirai)
|
120
|
+
# I quickly tried but I need to use the original backend instead of the moirai one
|
121
|
+
# The problem is that if we set a value that is the same as currently being used via fallback,
|
122
|
+
# it will create an entry in the database, and afterwards will try to add it in the PR, which we don't want.
|
123
|
+
def translation_same_as_current?
|
124
|
+
file_paths = KeyFinder.new.file_paths_for(translation_params[:key], locale: translation_params[:locale])
|
125
|
+
|
126
|
+
return false if file_paths.empty?
|
127
|
+
return false unless file_paths.all? { |file_path| File.exist?(file_path) }
|
128
|
+
|
129
|
+
translation_params[:value] == @file_handler.parse_file(file_paths.first)[translation_params[:key]]
|
130
|
+
end
|
131
|
+
|
132
|
+
def get_fallback_translation
|
133
|
+
file_paths = KeyFinder.new.file_paths_for(translation_params[:key], locale: translation_params[:locale])
|
134
|
+
|
135
|
+
return "" if file_paths.empty?
|
136
|
+
return "" unless file_paths.all? { |file_path| File.exist?(file_path) }
|
137
|
+
|
138
|
+
@file_handler.parse_file(file_paths.first)[translation_params[:key]]
|
139
|
+
end
|
92
140
|
end
|
93
141
|
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Prepares a certain file to be changefd in a Pull Request.
|
4
|
+
# It takes care of adjusting the file_path and content
|
5
|
+
module Moirai
|
6
|
+
class Change
|
7
|
+
attr_reader :file_path, :content
|
8
|
+
|
9
|
+
def initialize(file_path, content)
|
10
|
+
@file_path = file_path
|
11
|
+
@file_path = file_path.to_s.start_with?("./") ? file_path : "./#{file_path}"
|
12
|
+
@content = content.to_s.end_with?("\n") ? content : "#{content}\n"
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Moirai
|
4
|
+
class KeyFinder
|
5
|
+
include I18n::Backend::Base
|
6
|
+
|
7
|
+
# Mutex to ensure that concurrent translations loading will be thread-safe
|
8
|
+
MUTEX = Mutex.new
|
9
|
+
|
10
|
+
def initialize
|
11
|
+
load_translations
|
12
|
+
end
|
13
|
+
|
14
|
+
# TODO: remove locale default
|
15
|
+
# Returns all the file_paths where the key is found, including gems.
|
16
|
+
def file_paths_for(key, locale: I18n.locale)
|
17
|
+
return [] if key.blank?
|
18
|
+
|
19
|
+
locale ||= I18n.locale
|
20
|
+
moirai_translations[locale.to_sym].select do |_filename, data|
|
21
|
+
data.dig(*key.split(".")).present?
|
22
|
+
end.map { |k, _| k }.sort { |file_path| file_path.start_with?(Rails.root.to_s) ? 0 : 1 }
|
23
|
+
end
|
24
|
+
|
25
|
+
def store_moirai_translations(filename, locale, data, options)
|
26
|
+
moirai_translations[locale] ||= Concurrent::Hash.new
|
27
|
+
|
28
|
+
locale = locale.to_sym
|
29
|
+
moirai_translations[locale] ||= Concurrent::Hash.new
|
30
|
+
moirai_translations[locale][filename] = data.with_indifferent_access
|
31
|
+
end
|
32
|
+
|
33
|
+
def moirai_translations(do_init: false)
|
34
|
+
@moirai_translations ||= Concurrent::Hash.new do |h, k|
|
35
|
+
MUTEX.synchronize do
|
36
|
+
h[k] = Concurrent::Hash.new
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def load_file(filename)
|
42
|
+
type = File.extname(filename).tr(".", "").downcase
|
43
|
+
raise I18n::UnknownFileType.new(type, filename) unless respond_to?(:"load_#{type}", true)
|
44
|
+
data, keys_symbolized = send(:"load_#{type}", filename)
|
45
|
+
unless data.is_a?(Hash)
|
46
|
+
raise I18n::InvalidLocaleData.new(filename, "expects it to return a hash, but does not")
|
47
|
+
end
|
48
|
+
data.each do |locale, d|
|
49
|
+
store_moirai_translations(filename, locale, d || {}, skip_symbolize_keys: keys_symbolized)
|
50
|
+
end
|
51
|
+
|
52
|
+
data
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -2,13 +2,17 @@
|
|
2
2
|
|
3
3
|
module Moirai
|
4
4
|
class Translation < Moirai::ApplicationRecord
|
5
|
-
validates_presence_of :key, :locale, :
|
6
|
-
validate :file_path_must_exist
|
5
|
+
validates_presence_of :key, :locale, :value
|
7
6
|
|
8
|
-
|
7
|
+
# what if the key is present in multiple file_paths?
|
8
|
+
def file_path
|
9
|
+
@key_finder ||= KeyFinder.new
|
10
|
+
@key_finder.file_paths_for(key, locale: locale).first
|
11
|
+
end
|
9
12
|
|
10
|
-
def
|
11
|
-
|
13
|
+
def self.by_file_path(file_path)
|
14
|
+
key_finder = KeyFinder.new
|
15
|
+
all.select { |translation| key_finder.file_paths_for(translation.key, locale: translation.locale).include?(file_path) }
|
12
16
|
end
|
13
17
|
end
|
14
18
|
end
|