moirai 0.1.0 → 0.3.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 +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
|
+

|
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
|