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