moirai 0.1.0 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (37) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/test.yml +15 -6
  3. data/CHANGELOG.md +21 -0
  4. data/Gemfile +12 -1
  5. data/README.md +63 -27
  6. data/app/assets/config/moirai_manifest.js +0 -1
  7. data/app/assets/javascripts/moirai_translation_controller.js +41 -0
  8. data/app/assets/stylesheets/moirai/application.css +2 -0
  9. data/app/assets/stylesheets/translation_files.css +22 -0
  10. data/app/controllers/moirai/translation_files_controller.rb +66 -18
  11. data/app/controllers/moirai/translations_controller.rb +7 -0
  12. data/app/models/moirai/change.rb +15 -0
  13. data/app/models/moirai/key_finder.rb +55 -0
  14. data/app/models/moirai/translation.rb +9 -5
  15. data/app/models/moirai/translation_dumper.rb +40 -13
  16. data/app/views/layouts/moirai/application.html.erb +28 -1
  17. data/app/views/moirai/translation_files/_form.html.erb +3 -3
  18. data/app/views/moirai/translation_files/index.html.erb +1 -1
  19. data/app/views/moirai/translation_files/show.html.erb +11 -5
  20. data/app/views/moirai/translations/index.html.erb +7 -0
  21. data/bin/check +3 -1
  22. data/config/routes.rb +2 -1
  23. data/lib/generators/moirai/install_generator.rb +27 -2
  24. data/lib/generators/moirai/migration_generator.rb +5 -1
  25. data/lib/generators/moirai/templates/make_moirai_translations_file_path_not_required.rb.erb +5 -0
  26. data/lib/i18n/backend/moirai.rb +1 -53
  27. data/lib/i18n/extensions/i18n.rb +13 -0
  28. data/lib/moirai/engine.rb +8 -6
  29. data/lib/moirai/pull_request_creator.rb +42 -33
  30. data/lib/moirai/version.rb +1 -1
  31. data/lib/moirai.rb +2 -1
  32. data/moirai.gemspec +3 -6
  33. metadata +12 -77
  34. data/app/assets/javascripts/moirai/application.js +0 -2
  35. data/app/assets/javascripts/moirai/controllers/moirai_translation_controller.js +0 -27
  36. data/app/assets/javascripts/moirai/stimulus/init.js +0 -5
  37. /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: f735406747923f4a2493a81628e4cd88cf6f54d926fb31448ca5dab4ae70a8b9
4
- data.tar.gz: 40f69e72824a96f13fe4e67b753a36617a745cdca04c61e691c7cc7877c55b96
3
+ metadata.gz: e9d7f98e48e08b3e5208041fbba3344f5b280492754b7943e159d0e90e71d075
4
+ data.tar.gz: d4aa78c7ae9f0c27f18c3e760def299c5d8faf054b06423e300b2c095976ef0c
5
5
  SHA512:
6
- metadata.gz: 19eeb5b122adcd1f46fbfee0a39440f5d6ef9536bf28f8486f36fed40506d61449793adc6681cd3ce5eb35c10f9b5021928f97494e0c7063a5736fc8c0a2ea90
7
- data.tar.gz: 9e3d8298eac29bca6008af867b57f8e663d9128c07bccf531c400c2c2889109b15515b676b6bbd0d85144fae119cc087a5ad3bb6cec83564edae67907b73543c
6
+ metadata.gz: cb39e17d721373254b27086f4cf351c6145a3aa921809b8de0921ca82c661d53e0a1fb38da524c4fe012c2ecfcde7ffa2ec1029432e88d0cc43cf2f067c55341
7
+ data.tar.gz: 601b7c5353bbc71cd094223df88b74dc9f82d07657f306f8402160bc47f4d056940a28db55b8b88f4fd2275c23e66379e2778e2856f0d93a57defd7b706dc68c
@@ -1,11 +1,12 @@
1
1
  name: Test & lint
2
- on: [push, pull_request]
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.1, 3.2, 3.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: bin/check
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 press this button EVER” should be.
9
- - Automatically create Pull Requests based on these changes, saving your developers from yet another “small tweakemail request.
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 EVERshould 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, as well as inserting the engine in the `routes.rb` file:
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 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.
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
- If you want to only allow specific users to perform inline editing, you can override the `moirai_edit_enabled?` method in your application helper.
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" || current_user&.admin?
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 codebase,
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 **environment variables** (this is explained below).
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 and pull requests.
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
- - 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.
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
- - `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.
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 convention. For example:
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 updated.
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
- * Performance of translations lookup
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
@@ -1,2 +1 @@
1
1
  //= link_directory ../stylesheets/moirai .css
2
- //= link_directory ../javascripts/moirai .js
@@ -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
+ }
@@ -12,4 +12,6 @@
12
12
  *
13
13
  *= require_tree .
14
14
  *= require_self
15
+ *= require translation_files
16
+
15
17
  */
@@ -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(@decoded_path)
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(file_path: translation_params[:file_path],
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 PR"
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
- 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?
40
+ if translation_params[:value].blank? || translation_same_as_current?
42
41
  translation.destroy
43
- flash.notice = "Translation #{translation.key} was successfully deleted."
44
- redirect_to_translation_file(translation.file_path)
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
- redirect_to_translation_file(translation.file_path)
62
+ success_response(translation)
55
63
  end
56
64
 
57
65
  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]
66
+ if translation_same_as_current?
60
67
  flash.alert = "Translation #{translation_params[:key]} already exists."
61
- redirect_to_translation_file(translation_params[:file_path])
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
- translation.locale = @file_handler.get_first_key(translation_params[:file_path])
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
- redirect_to_translation_file(translation.file_path)
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[:id]]
82
- @decoded_path = CGI.unescape(@file_path)
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, :file_path)
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,7 @@
1
+ module Moirai
2
+ class TranslationsController < ApplicationController
3
+ def index
4
+ @translations = Translation.order(created_at: :desc).pluck(:locale, :key, :value)
5
+ end
6
+ end
7
+ 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, :file_path
6
- validate :file_path_must_exist
5
+ validates_presence_of :key, :locale, :value
7
6
 
8
- private
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 file_path_must_exist
11
- errors.add(:file_path, "must exist") unless file_path && File.exist?(file_path)
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