locale_ninja 0.1.2
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 +7 -0
- data/LICENSE +20 -0
- data/README.md +93 -0
- data/Rakefile +10 -0
- data/app/assets/builds/locale_ninja.css +1 -0
- data/app/assets/config/locale_ninja_manifest.js +1 -0
- data/app/assets/stylesheets/application.tailwind.css +17 -0
- data/app/assets/stylesheets/locale_ninja/application.css +26 -0
- data/app/controllers/locale_ninja/application_controller.rb +35 -0
- data/app/controllers/locale_ninja/branches_controller.rb +22 -0
- data/app/controllers/locale_ninja/dashboard_controller.rb +19 -0
- data/app/controllers/locale_ninja/locales_controller.rb +33 -0
- data/app/controllers/locale_ninja/session_controller.rb +34 -0
- data/app/helpers/locale_ninja/application_helper.rb +9 -0
- data/app/helpers/locale_ninja/dashboard_helper.rb +6 -0
- data/app/helpers/locale_ninja/locale_helper.rb +91 -0
- data/app/jobs/locale_ninja/application_job.rb +6 -0
- data/app/mailers/locale_ninja/application_mailer.rb +8 -0
- data/app/models/locale_ninja/application_record.rb +7 -0
- data/app/services/locale_ninja/github_api_service.rb +98 -0
- data/app/views/components/_branch_select.html.erb +10 -0
- data/app/views/components/_flashes.html.erb +33 -0
- data/app/views/components/_sidebar.html.erb +31 -0
- data/app/views/components/_table.html.erb +204 -0
- data/app/views/layouts/locale_ninja/application.html.erb +15 -0
- data/app/views/locale_ninja/branches/index.html.erb +1 -0
- data/app/views/locale_ninja/branches/show.html.erb +22 -0
- data/app/views/locale_ninja/dashboard/index.html.erb +34 -0
- data/app/views/locale_ninja/locales/show.html.erb +21 -0
- data/config/credentials/development.key +1 -0
- data/config/credentials/development.yml.enc +1 -0
- data/config/credentials/production.yml.enc +1 -0
- data/config/locales/locale_ninja/en.yml +6 -0
- data/config/locales/locale_ninja/fr.yml +6 -0
- data/config/routes.rb +13 -0
- data/config/tailwind.config.js +23 -0
- data/lib/locale_ninja/engine.rb +11 -0
- data/lib/locale_ninja/version.rb +6 -0
- data/lib/locale_ninja.rb +8 -0
- data/lib/tasks/locale_ninja_tasks.rake +21 -0
- metadata +190 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: e9cb6c35b765aebbf37151c45621097c3de4f820e11460a44907b156b9b67d9f
|
4
|
+
data.tar.gz: e303d7e92adb1ecc59760bde0556385e899127e557a834722c3604ce27233570
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: f0cea58d0d9c1428cce05e007c1b6988e1b47fd5847fa053b8c76c83f68dd56adebe07aedc3009792ea0f9718c99f4c0a62fe2810333d13af905e0bea126c11c
|
7
|
+
data.tar.gz: b02b41469d821255b749fbebb7e39d41ef03fe46c6c5152705f43de9ceb76d4f7a4b87136d42bfa7ba509836578aab4595c8449bf49f377a9fcba126c76b52bb
|
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright 2023 Squadracer
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,93 @@
|
|
1
|
+
[](https://badge.fury.io/rb/locale_ninja)
|
2
|
+
[](https://github.com/squadracer/locale_ninja/actions/workflows/rubyonrails.yml)
|
3
|
+
|
4
|
+
# 🥷 LocaleNinja
|
5
|
+
|
6
|
+
A Git-based gem to manage translations in your Ruby on Rails app.
|
7
|
+
|
8
|
+
LocaleNinja simplifies the management of translations on a website. Unlike traditional solutions that require connecting to an external platform, LocaleNinja is a Git-based gem installed directly in your project, allowing you to maintain full control over your translations without relying on a third-party service.
|
9
|
+
|
10
|
+
<br/>
|
11
|
+
|
12
|
+
## ✨ Key Features
|
13
|
+
**Streamlined Translation Management:** LocaleNinja provides a user-friendly interface to effortlessly handle all your website translations within the same project.
|
14
|
+
|
15
|
+
**Seamless Git Integration:** LocaleNinja connects to your Git repository and automatically handles pull and push of translation files. This ensures smooth collaboration with developers and simplifies the process of updating translations.
|
16
|
+
|
17
|
+
<br/>
|
18
|
+
|
19
|
+
## 💻 Installation
|
20
|
+
Add this line to your application's Gemfile:
|
21
|
+
|
22
|
+
```ruby
|
23
|
+
gem "locale_ninja"
|
24
|
+
```
|
25
|
+
|
26
|
+
And then execute:
|
27
|
+
```bash
|
28
|
+
$ bundle
|
29
|
+
```
|
30
|
+
|
31
|
+
Or install it yourself as:
|
32
|
+
```bash
|
33
|
+
$ gem install locale_ninja
|
34
|
+
```
|
35
|
+
|
36
|
+
<br/>
|
37
|
+
|
38
|
+
## ⚙️ Setup
|
39
|
+
|
40
|
+
To setup LocalNinja you will need to create a [github app](https://docs.github.com/en/apps/creating-github-apps/registering-a-github-app/registering-a-github-app) in your repository, it will allow your app to commit to your repo. When you are on the github app form, here are some steps specific to our application to follow :
|
41
|
+
|
42
|
+
- In the "Identifying and authorizing users" section, your callback url will be : `your-domain-name.com/locale_ninja/github`
|
43
|
+
- In the "Webhook" section switch off the "active" checkbox
|
44
|
+
- In the "Permissions" section, you will have to:
|
45
|
+
- Switch "Content" permissions to "Read and write"
|
46
|
+
- Switch "Metadata" permissions to "Read-only"
|
47
|
+
- Switch "Pull requests" permissions to "Read-only"
|
48
|
+
|
49
|
+
<br/>
|
50
|
+
|
51
|
+
Once done you will have access to your `client_id` and `client_secret`. You can then run :
|
52
|
+
|
53
|
+
```sh
|
54
|
+
bin/rails credentials:edit
|
55
|
+
```
|
56
|
+
|
57
|
+
And add this, in the editor that just open-up :
|
58
|
+
```yaml
|
59
|
+
github:
|
60
|
+
repository_name: organization/repository_name
|
61
|
+
client_secret: <40 bytes long secret key>
|
62
|
+
client_id: <20 bytes long id>
|
63
|
+
```
|
64
|
+
|
65
|
+
You can then close the editor, this will generate a `master.key`file and a `credentials.yml.enc` file in the config folder. Now your connexion with github is totally setup.
|
66
|
+
|
67
|
+
<br/>
|
68
|
+
|
69
|
+
You now just have to add this in your routes :
|
70
|
+
```ruby
|
71
|
+
#config/routes.rb
|
72
|
+
mount LocaleNinja::Engine => '/locale_ninja'
|
73
|
+
```
|
74
|
+
|
75
|
+
<br/>
|
76
|
+
|
77
|
+
Your translation manager will be accessible at `your-domain-name/locale_ninja` or `localhost:3000/locale_ninja` 🎉
|
78
|
+
|
79
|
+
## 👥 Contributors
|
80
|
+
|
81
|
+
<table>
|
82
|
+
<tbody>
|
83
|
+
<tr>
|
84
|
+
<td align="center" valign="top" width="25%"><a href="https://twitter.com/julienmarseil"><img src="https://avatars.githubusercontent.com/u/18447285?v=4" width="100px;" alt="Julien Marseille"/><br /><sub><b>Julien Marseille</b></sub></a></td>
|
85
|
+
<td align="center" valign="top" width="25%"><a href="https://twitter.com/ClementAvenel"><img src="https://avatars.githubusercontent.com/u/29872940?v=4" width="100px;" alt="Clément Avenel"/><br /><sub><b>Clément Avenel</b></sub></a></td>
|
86
|
+
<td align="center" valign="top" width="25%"><a href="https://www.linkedin.com/in/pierre-fitoussi-267133135/"><img src="https://avatars.githubusercontent.com/u/79254731?v=4" width="100px;" alt="Pierre Fitoussi"/><br /><sub><b>Pierre Fitoussi</b></sub></a></td>
|
87
|
+
<td align="center" valign="top" width="25%"><a href="https://twitter.com/masterpoo_dev"><img src="https://avatars.githubusercontent.com/u/92919588?v=4" width="100px;" alt="Théo Dupuis"/><br /><sub><b>Théo Dupuis</b></sub></a></td>
|
88
|
+
</tr>
|
89
|
+
</table>
|
90
|
+
|
91
|
+
|
92
|
+
## License
|
93
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
/*! tailwindcss v3.3.3 | MIT License | https://tailwindcss.com*/*,:after,:before{border:0 solid #e5e7eb;box-sizing:border-box}:after,:before{--tw-content:""}html{-webkit-text-size-adjust:100%;font-feature-settings:normal;font-family:Inter var,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;font-variation-settings:normal;line-height:1.5;-moz-tab-size:4;-o-tab-size:4;tab-size:4}body{line-height:inherit;margin:0}hr{border-top-width:1px;color:inherit;height:0}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:initial}sub{bottom:-.25em}sup{top:-.5em}table{border-collapse:collapse;border-color:inherit;text-indent:0}button,input,optgroup,select,textarea{font-feature-settings:inherit;color:inherit;font-family:inherit;font-size:100%;font-variation-settings:inherit;font-weight:inherit;line-height:inherit;margin:0;padding:0}button,select{text-transform:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button;background-color:initial;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:initial}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}fieldset{margin:0}fieldset,legend{padding:0}menu,ol,ul{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{color:#9ca3af;opacity:1}input::placeholder,textarea::placeholder{color:#9ca3af;opacity:1}[role=button],button{cursor:pointer}:disabled{cursor:default}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{height:auto;max-width:100%}[hidden]{display:none}[multiple],[type=date],[type=datetime-local],[type=email],[type=month],[type=number],[type=password],[type=search],[type=tel],[type=text],[type=time],[type=url],[type=week],select,textarea{--tw-shadow:0 0 #0000;-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:#fff;border-color:#6b7280;border-radius:0;border-width:1px;font-size:1rem;line-height:1.5rem;padding:.5rem .75rem}[multiple]:focus,[type=date]:focus,[type=datetime-local]:focus,[type=email]:focus,[type=month]:focus,[type=number]:focus,[type=password]:focus,[type=search]:focus,[type=tel]:focus,[type=text]:focus,[type=time]:focus,[type=url]:focus,[type=week]:focus,select:focus,textarea:focus{--tw-ring-inset:var(--tw-empty,/*!*/ /*!*/);--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:#2563eb;--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);border-color:#2563eb;box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);outline:2px solid #0000;outline-offset:2px}input::-moz-placeholder,textarea::-moz-placeholder{color:#6b7280;opacity:1}input::placeholder,textarea::placeholder{color:#6b7280;opacity:1}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-date-and-time-value{min-height:1.5em}::-webkit-datetime-edit,::-webkit-datetime-edit-day-field,::-webkit-datetime-edit-hour-field,::-webkit-datetime-edit-meridiem-field,::-webkit-datetime-edit-millisecond-field,::-webkit-datetime-edit-minute-field,::-webkit-datetime-edit-month-field,::-webkit-datetime-edit-second-field,::-webkit-datetime-edit-year-field{padding-bottom:0;padding-top:0}select{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3E%3Cpath stroke='%236b7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='m6 8 4 4 4-4'/%3E%3C/svg%3E");background-position:right .5rem center;background-repeat:no-repeat;background-size:1.5em 1.5em;padding-right:2.5rem;-webkit-print-color-adjust:exact;print-color-adjust:exact}[multiple]{background-image:none;background-position:0 0;background-repeat:unset;background-size:initial;padding-right:.75rem;-webkit-print-color-adjust:unset;print-color-adjust:unset}[type=checkbox],[type=radio]{--tw-shadow:0 0 #0000;-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:#fff;background-origin:border-box;border-color:#6b7280;border-width:1px;color:#2563eb;display:inline-block;flex-shrink:0;height:1rem;padding:0;-webkit-print-color-adjust:exact;print-color-adjust:exact;-webkit-user-select:none;-moz-user-select:none;user-select:none;vertical-align:middle;width:1rem}[type=checkbox]{border-radius:0}[type=radio]{border-radius:100%}[type=checkbox]:focus,[type=radio]:focus{--tw-ring-inset:var(--tw-empty,/*!*/ /*!*/);--tw-ring-offset-width:2px;--tw-ring-offset-color:#fff;--tw-ring-color:#2563eb;--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);outline:2px solid #0000;outline-offset:2px}[type=checkbox]:checked,[type=radio]:checked{background-color:currentColor;background-position:50%;background-repeat:no-repeat;background-size:100% 100%;border-color:#0000}[type=checkbox]:checked{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%23fff' viewBox='0 0 16 16'%3E%3Cpath d='M12.207 4.793a1 1 0 0 1 0 1.414l-5 5a1 1 0 0 1-1.414 0l-2-2a1 1 0 0 1 1.414-1.414L6.5 9.086l4.293-4.293a1 1 0 0 1 1.414 0z'/%3E%3C/svg%3E")}[type=radio]:checked{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%23fff' viewBox='0 0 16 16'%3E%3Ccircle cx='8' cy='8' r='3'/%3E%3C/svg%3E")}[type=checkbox]:checked:focus,[type=checkbox]:checked:hover,[type=checkbox]:indeterminate,[type=radio]:checked:focus,[type=radio]:checked:hover{background-color:currentColor;border-color:#0000}[type=checkbox]:indeterminate{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 16 16'%3E%3Cpath stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M4 8h8'/%3E%3C/svg%3E");background-position:50%;background-repeat:no-repeat;background-size:100% 100%}[type=checkbox]:indeterminate:focus,[type=checkbox]:indeterminate:hover{background-color:currentColor;border-color:#0000}[type=file]{background:unset;border-color:inherit;border-radius:0;border-width:0;font-size:unset;line-height:inherit;padding:0}[type=file]:focus{outline:1px solid ButtonText;outline:1px auto -webkit-focus-ring-color}*,::backdrop,:after,:before{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:#3b82f680;--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: }.container{width:100%}@media (min-width:640px){.container{max-width:640px}}@media (min-width:768px){.container{max-width:768px}}@media (min-width:1024px){.container{max-width:1024px}}@media (min-width:1280px){.container{max-width:1280px}}@media (min-width:1536px){.container{max-width:1536px}}.sr-only{clip:rect(0,0,0,0);border-width:0;height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;white-space:nowrap;width:1px}.fixed{position:fixed}.relative{position:relative}.bottom-12{bottom:3rem}.right-12{right:3rem}.z-20{z-index:20}.-mx-1{margin-left:-.25rem;margin-right:-.25rem}.-mx-4{margin-left:-1rem;margin-right:-1rem}.-my-2{margin-bottom:-.5rem;margin-top:-.5rem}.mx-4{margin-left:1rem;margin-right:1rem}.my-2{margin-bottom:.5rem;margin-top:.5rem}.my-4{margin-bottom:1rem;margin-top:1rem}.my-6{margin-bottom:1.5rem;margin-top:1.5rem}.my-8{margin-bottom:2rem;margin-top:2rem}.mb-12{margin-bottom:3rem}.mb-8{margin-bottom:2rem}.mr-1{margin-right:.25rem}.mr-4{margin-right:1rem}.mr-8{margin-right:2rem}.mt-2{margin-top:.5rem}.mt-4{margin-top:1rem}.mt-5{margin-top:1.25rem}.mt-6{margin-top:1.5rem}.block{display:block}.inline-block{display:inline-block}.flex{display:flex}.table{display:table}.h-2{height:.5rem}.h-2\.5{height:.625rem}.h-3{height:.75rem}.h-5{height:1.25rem}.h-6{height:1.5rem}.h-9{height:2.25rem}.h-screen{height:100vh}.min-h-screen{min-height:100vh}.w-1\/2{width:50%}.w-48{width:12rem}.w-5{width:1.25rem}.w-6{width:1.5rem}.w-64{width:16rem}.w-80{width:20rem}.w-9{width:2.25rem}.w-full{width:100%}.min-w-full{min-width:100%}.max-w-sm{max-width:24rem}.flex-1{flex:1 1 0%}.shrink-0{flex-shrink:0}.transform{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.cursor-pointer{cursor:pointer}.flex-row{flex-direction:row}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-start{align-items:flex-start}.items-center{align-items:center}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-4{gap:1rem}.gap-x-2{-moz-column-gap:.5rem;column-gap:.5rem}.gap-x-3{-moz-column-gap:.75rem;column-gap:.75rem}.gap-x-4{-moz-column-gap:1rem;column-gap:1rem}.space-y-8>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-bottom:calc(2rem*var(--tw-space-y-reverse));margin-top:calc(2rem*(1 - var(--tw-space-y-reverse)))}.divide-y>:not([hidden])~:not([hidden]){--tw-divide-y-reverse:0;border-bottom-width:calc(1px*var(--tw-divide-y-reverse));border-top-width:calc(1px*(1 - var(--tw-divide-y-reverse)))}.divide-gray-200>:not([hidden])~:not([hidden]){--tw-divide-opacity:1;border-color:rgb(229 231 235/var(--tw-divide-opacity))}.self-center{align-self:center}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.overflow-y-auto{overflow-y:auto}.whitespace-nowrap{white-space:nowrap}.rounded{border-radius:.25rem}.rounded-full{border-radius:9999px}.rounded-lg{border-radius:.5rem}.rounded-md{border-radius:.375rem}.rounded-xl{border-radius:.75rem}.border{border-width:1px}.border-2{border-width:2px}.border-r{border-right-width:1px}.border-t-4{border-top-width:4px}.border-solid{border-style:solid}.border-blue-600{--tw-border-opacity:1;border-color:rgb(37 99 235/var(--tw-border-opacity))}.border-gray-100{--tw-border-opacity:1;border-color:rgb(243 244 246/var(--tw-border-opacity))}.border-gray-200{--tw-border-opacity:1;border-color:rgb(229 231 235/var(--tw-border-opacity))}.border-gray-300{--tw-border-opacity:1;border-color:rgb(209 213 219/var(--tw-border-opacity))}.border-white{--tw-border-opacity:1;border-color:rgb(255 255 255/var(--tw-border-opacity))}.bg-blue-100{--tw-bg-opacity:1;background-color:rgb(219 234 254/var(--tw-bg-opacity))}.bg-blue-50{--tw-bg-opacity:1;background-color:rgb(239 246 255/var(--tw-bg-opacity))}.bg-blue-500{--tw-bg-opacity:1;background-color:rgb(59 130 246/var(--tw-bg-opacity))}.bg-blue-600{--tw-bg-opacity:1;background-color:rgb(37 99 235/var(--tw-bg-opacity))}.bg-gray-200{--tw-bg-opacity:1;background-color:rgb(229 231 235/var(--tw-bg-opacity))}.bg-gray-50{--tw-bg-opacity:1;background-color:rgb(249 250 251/var(--tw-bg-opacity))}.bg-white{--tw-bg-opacity:1;background-color:rgb(255 255 255/var(--tw-bg-opacity))}.object-cover{-o-object-fit:cover;object-fit:cover}.p-2{padding:.5rem}.p-2\.5{padding:.625rem}.p-4{padding:1rem}.p-8{padding:2rem}.px-1{padding-left:.25rem;padding-right:.25rem}.px-4{padding-left:1rem;padding-right:1rem}.px-5{padding-left:1.25rem;padding-right:1.25rem}.px-6{padding-left:1.5rem;padding-right:1.5rem}.py-1{padding-bottom:.25rem;padding-top:.25rem}.py-2{padding-bottom:.5rem;padding-top:.5rem}.py-2\.5{padding-bottom:.625rem;padding-top:.625rem}.py-3{padding-bottom:.75rem;padding-top:.75rem}.py-3\.5{padding-bottom:.875rem;padding-top:.875rem}.py-4{padding-bottom:1rem;padding-top:1rem}.py-8{padding-bottom:2rem;padding-top:2rem}.text-left{text-align:left}.text-center{text-align:center}.align-middle{vertical-align:middle}.text-5xl{font-size:3rem;line-height:1}.text-lg{font-size:1.125rem;line-height:1.75rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xl{font-size:1.25rem;line-height:1.75rem}.text-xs{font-size:.75rem;line-height:1rem}.font-bold{font-weight:700}.font-light{font-weight:300}.font-medium{font-weight:500}.font-normal{font-weight:400}.uppercase{text-transform:uppercase}.capitalize{text-transform:capitalize}.text-blue-600{--tw-text-opacity:1;color:rgb(37 99 235/var(--tw-text-opacity))}.text-gray-400{--tw-text-opacity:1;color:rgb(156 163 175/var(--tw-text-opacity))}.text-gray-500{--tw-text-opacity:1;color:rgb(107 114 128/var(--tw-text-opacity))}.text-gray-600{--tw-text-opacity:1;color:rgb(75 85 99/var(--tw-text-opacity))}.text-gray-700{--tw-text-opacity:1;color:rgb(55 65 81/var(--tw-text-opacity))}.text-gray-800{--tw-text-opacity:1;color:rgb(31 41 55/var(--tw-text-opacity))}.text-gray-900{--tw-text-opacity:1;color:rgb(17 24 39/var(--tw-text-opacity))}.text-green-600{--tw-text-opacity:1;color:rgb(22 163 74/var(--tw-text-opacity))}.text-red-500{--tw-text-opacity:1;color:rgb(239 68 68/var(--tw-text-opacity))}.text-white{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity))}.placeholder-gray-400\/70::-moz-placeholder{color:#9ca3afb3}.placeholder-gray-400\/70::placeholder{color:#9ca3afb3}.shadow-md{--tw-shadow:0 4px 6px -1px #0000001a,0 2px 4px -2px #0000001a;--tw-shadow-colored:0 4px 6px -1px var(--tw-shadow-color),0 2px 4px -2px var(--tw-shadow-color)}.shadow-md,.shadow-xl{box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.shadow-xl{--tw-shadow:0 20px 25px -5px #0000001a,0 8px 10px -6px #0000001a;--tw-shadow-colored:0 20px 25px -5px var(--tw-shadow-color),0 8px 10px -6px var(--tw-shadow-color)}.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.transition-colors{transition-duration:.15s;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1)}.duration-200{transition-duration:.2s}.duration-300{transition-duration:.3s}h1{font-size:2.25rem;font-weight:700;line-height:2.5rem}.hover\:bg-blue-600:hover{--tw-bg-opacity:1;background-color:rgb(37 99 235/var(--tw-bg-opacity))}.hover\:bg-gray-100:hover{--tw-bg-opacity:1;background-color:rgb(243 244 246/var(--tw-bg-opacity))}.hover\:text-gray-700:hover{--tw-text-opacity:1;color:rgb(55 65 81/var(--tw-text-opacity))}.hover\:underline:hover{text-decoration-line:underline}.focus\:border-blue-400:focus{--tw-border-opacity:1;border-color:rgb(96 165 250/var(--tw-border-opacity))}.focus\:border-blue-500:focus{--tw-border-opacity:1;border-color:rgb(59 130 246/var(--tw-border-opacity))}.focus\:outline-none:focus{outline:2px solid #0000;outline-offset:2px}.focus\:ring:focus{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(3px + var(--tw-ring-offset-width)) var(--tw-ring-color)}.focus\:ring-2:focus,.focus\:ring:focus{box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000)}.focus\:ring-2:focus{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color)}.focus\:ring-blue-300:focus{--tw-ring-opacity:1;--tw-ring-color:rgb(147 197 253/var(--tw-ring-opacity))}.focus\:ring-blue-400:focus{--tw-ring-opacity:1;--tw-ring-color:rgb(96 165 250/var(--tw-ring-opacity))}.focus\:ring-blue-500:focus{--tw-ring-opacity:1;--tw-ring-color:rgb(59 130 246/var(--tw-ring-opacity))}.focus\:ring-opacity-40:focus{--tw-ring-opacity:0.4}.focus\:ring-offset-2:focus{--tw-ring-offset-width:2px}:is([dir=rtl] .rtl\:-scale-x-100){--tw-scale-x:-1;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}:is([dir=rtl] .rtl\:border-l){border-left-width:1px}:is([dir=rtl] .rtl\:border-r-0){border-right-width:0}:is([dir=rtl] .rtl\:text-right){text-align:right}@media (min-width:640px){.sm\:-mx-6{margin-left:-1.5rem;margin-right:-1.5rem}.sm\:mt-0{margin-top:0}.sm\:flex{display:flex}.sm\:w-auto{width:auto}.sm\:items-center{align-items:center}.sm\:justify-between{justify-content:space-between}}@media (min-width:768px){.md\:rounded-lg{border-radius:.5rem}.md\:px-6{padding-left:1.5rem;padding-right:1.5rem}}@media (min-width:1024px){.lg\:-mx-8{margin-left:-2rem;margin-right:-2rem}.lg\:px-8{padding-left:2rem;padding-right:2rem}}
|
@@ -0,0 +1 @@
|
|
1
|
+
//= link_tree ../builds/ .css
|
@@ -0,0 +1,26 @@
|
|
1
|
+
/*
|
2
|
+
* This is a manifest file that'll be compiled into application.css, which will include all the files
|
3
|
+
* listed below.
|
4
|
+
*
|
5
|
+
* Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets,
|
6
|
+
* or any plugin's vendor/assets/stylesheets directory can be referenced here using a relative path.
|
7
|
+
*
|
8
|
+
* You're free to add application-wide styles to this file and they'll appear at the bottom of the
|
9
|
+
* compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS
|
10
|
+
* files in this directory. Styles in this file should be added after the last require_* statement.
|
11
|
+
* It is generally better to create a new file per style scope.
|
12
|
+
*
|
13
|
+
*= require_tree .
|
14
|
+
*= require_self
|
15
|
+
*/
|
16
|
+
|
17
|
+
@keyframes fade-out { 0% {opacity: 1;} 90% {opacity: 1;} 100% {opacity: 0;} }
|
18
|
+
|
19
|
+
.fade-out {
|
20
|
+
animation-name: fade-out;
|
21
|
+
}
|
22
|
+
|
23
|
+
.animated {
|
24
|
+
animation-duration: 5s;
|
25
|
+
animation-fill-mode: both;
|
26
|
+
}
|
@@ -0,0 +1,35 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module LocaleNinja
|
4
|
+
require 'octokit'
|
5
|
+
require 'httparty'
|
6
|
+
class ApplicationController < ActionController::Base
|
7
|
+
add_flash_types :alert, :info, :error, :warning, :success
|
8
|
+
before_action :authenticate!, skip: :authenticate!
|
9
|
+
rescue_from ::Octokit::Unauthorized, with: :clear_session
|
10
|
+
|
11
|
+
CLIENT_ID = Rails.application.credentials.github.client_id
|
12
|
+
private_constant :CLIENT_ID
|
13
|
+
|
14
|
+
private
|
15
|
+
|
16
|
+
def clear_session
|
17
|
+
session.clear
|
18
|
+
redirect_to(dashboard_index_path)
|
19
|
+
end
|
20
|
+
|
21
|
+
def set_client
|
22
|
+
@client = GithubApiService.new(access_token:)
|
23
|
+
end
|
24
|
+
|
25
|
+
def access_token
|
26
|
+
session[:access_token]
|
27
|
+
end
|
28
|
+
|
29
|
+
def authenticate!
|
30
|
+
return if access_token
|
31
|
+
|
32
|
+
redirect_to("https://github.com/login/oauth/authorize?scope=repo,user&client_id=#{CLIENT_ID}", allow_other_host: true)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module LocaleNinja
|
4
|
+
class BranchesController < ApplicationController
|
5
|
+
before_action :set_client, only: %i[index show]
|
6
|
+
|
7
|
+
def index
|
8
|
+
redirect_to(branch_path(@client.default_branch))
|
9
|
+
end
|
10
|
+
|
11
|
+
def select
|
12
|
+
redirect_to(branch_path(params[:branch_id]))
|
13
|
+
end
|
14
|
+
|
15
|
+
def show
|
16
|
+
@branches = @client.public_branches
|
17
|
+
@branch_name = params[:id]
|
18
|
+
locales_yml = @client.pull(branch: @branch_name).values.map { YAML.load(_1) }
|
19
|
+
@code_value_by_locales = locales_yml.to_h { [_1.keys[0], LocaleHelper.traverse(_1)] }
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module LocaleNinja
|
4
|
+
class DashboardController < ApplicationController
|
5
|
+
before_action :set_client, only: [:index]
|
6
|
+
|
7
|
+
def index
|
8
|
+
@locales_count = LocaleHelper.locales_count(@client)
|
9
|
+
translation_branches = @client.branches.filter { |branch| branch.ends_with?('__translations') }
|
10
|
+
@branches_count = @client.branches.count - translation_branches.count
|
11
|
+
@commits_count = translation_branches.sum do |branch|
|
12
|
+
@client.client.commits_since(@client.repository_fullname,
|
13
|
+
1.month.ago.strftime('%Y-%m-%d'),
|
14
|
+
sha_or_branch: branch
|
15
|
+
).count
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module LocaleNinja
|
4
|
+
require 'json'
|
5
|
+
require 'cgi'
|
6
|
+
class LocalesController < ApplicationController
|
7
|
+
before_action :set_client, only: %i[show update]
|
8
|
+
|
9
|
+
CLIENT_ID = Rails.application.credentials.github.client_id
|
10
|
+
CLIENT_SECRET = Rails.application.credentials.github.client_secret
|
11
|
+
|
12
|
+
private_constant :CLIENT_ID
|
13
|
+
private_constant :CLIENT_SECRET
|
14
|
+
|
15
|
+
def show
|
16
|
+
@locale = params[:locale]
|
17
|
+
@branch_name = params[:branch_id]
|
18
|
+
@source, @target = LocaleHelper.all_keys_for_locales(@client, [I18n.default_locale.to_s, @locale])
|
19
|
+
@translations = @target.zip(@source)
|
20
|
+
end
|
21
|
+
|
22
|
+
def update
|
23
|
+
@branch_name = params[:branch_id]
|
24
|
+
translation_keys = params[:val].permit!.to_h.compact_blank
|
25
|
+
yml = LocaleHelper.keys2yml(translation_keys)
|
26
|
+
yml.each { |path, file| @client.push(path, file, branch: @branch_name) }
|
27
|
+
@client.pull_request(@branch_name)
|
28
|
+
flash[:success] = t('.success')
|
29
|
+
|
30
|
+
redirect_to(branch_path(@branch_name))
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module LocaleNinja
|
4
|
+
class SessionController < ApplicationController
|
5
|
+
skip_before_action :authenticate!, only: %i[login logout]
|
6
|
+
|
7
|
+
CLIENT_ID = Rails.application.credentials.github.client_id
|
8
|
+
CLIENT_SECRET = Rails.application.credentials.github.client_secret
|
9
|
+
|
10
|
+
private_constant :CLIENT_ID
|
11
|
+
private_constant :CLIENT_SECRET
|
12
|
+
|
13
|
+
def login
|
14
|
+
code = params['code']
|
15
|
+
response = ::HTTParty.post('https://github.com/login/oauth/access_token',
|
16
|
+
body: {
|
17
|
+
client_id: CLIENT_ID,
|
18
|
+
client_secret: CLIENT_SECRET,
|
19
|
+
code:
|
20
|
+
}
|
21
|
+
)
|
22
|
+
parsed = CGI.parse(response)
|
23
|
+
session[:access_token] = parsed['access_token'].first
|
24
|
+
user = GithubApiService.new(access_token:).user
|
25
|
+
session[:user] = user.to_h.slice(:avatar_url, :id, :login)
|
26
|
+
redirect_to(root_path)
|
27
|
+
end
|
28
|
+
|
29
|
+
def logout
|
30
|
+
session.clear
|
31
|
+
redirect_to('https://github.com/logout')
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,91 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module LocaleNinja
|
4
|
+
module LocaleHelper
|
5
|
+
def self.keys2yml(translation_keys)
|
6
|
+
files = translation_keys.group_by { |key, _| key.split('$').first }.transform_values(&:to_h)
|
7
|
+
files.transform_values! { |translations| translations.transform_keys { |key| key.split('$').last } }
|
8
|
+
files.transform_values! do |file|
|
9
|
+
file.each_with_object({}) do |(key, value), hash|
|
10
|
+
hash.deep_merge!(key.split('.').reverse.reduce(value) { |a, n| { n => a } })
|
11
|
+
end.to_yaml
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.locales_count(github_service)
|
16
|
+
github_service.locale_files_path.uniq { |x| x.scan(/\w.yml/) }.size
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.all_keys(github_service, branch: 'translations')
|
20
|
+
locales_yml = github_service.pull(branch:).transform_values { |file| YAML.load(file) }
|
21
|
+
locales_list = locales_yml.values.map(&:keys).flatten.uniq
|
22
|
+
locales_yml.flat_map do |path, file|
|
23
|
+
path = path.gsub(/\b(#{locales_list.join('|')})\b/, '%<locale>s')
|
24
|
+
hash2keys(file.values.first).map { |key| "#{path}$%<locale>s.#{key}" }
|
25
|
+
end.uniq
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.all_keys_for_locales(github_service, locales, branch: 'translations')
|
29
|
+
locales_yml = github_service.pull(branch:).transform_values { |file| YAML.load(file) }
|
30
|
+
locales_list = locales_yml.values.map(&:keys).flatten.uniq
|
31
|
+
locales_yml.transform_values! { |hash| [hash.keys.first, traverse(hash.values.first).to_h] }
|
32
|
+
generic_keys = locales_yml.flat_map do |path, file|
|
33
|
+
path = path.gsub(/\b(#{locales_list.join('|')})\b/, '%<locale>s')
|
34
|
+
_, hash = file
|
35
|
+
hash.map { |key, _hash| "#{path}$%<locale>s.#{key}" }
|
36
|
+
end.uniq
|
37
|
+
translations = locales_yml.flat_map do |path, (locale, hash)|
|
38
|
+
hash.map do |key, value|
|
39
|
+
["#{path}$#{locale}.#{key}", value]
|
40
|
+
end
|
41
|
+
end.to_h
|
42
|
+
|
43
|
+
locales.map do |locale|
|
44
|
+
generic_keys.to_h do |key|
|
45
|
+
locale_key = format(key, locale:)
|
46
|
+
[locale_key, translations[locale_key]]
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def self.missing_keys(locale, github_service, branch:)
|
52
|
+
generic_keys = all_keys(github_service, branch:)
|
53
|
+
locale_yml = pull_one_locale(locale, github_service, branch:)
|
54
|
+
locale_keys = locale_yml.flat_map do |path, file|
|
55
|
+
hash2keys(file).map { |key| "#{path}$#{key}" }
|
56
|
+
end.uniq
|
57
|
+
generic_keys.map { |key| format(key, locale:) } - locale_keys
|
58
|
+
end
|
59
|
+
|
60
|
+
def self.pull_one_locale(locale, github_service, branch: 'translations')
|
61
|
+
locale_files_path = github_service.locale_files_path(branch:).filter { |path| path.ends_with?("#{locale}.yml") }
|
62
|
+
github_service.pull(locale_files_path, branch:).transform_values { |file| YAML.load(file) }
|
63
|
+
end
|
64
|
+
|
65
|
+
def self.traverse(hash, parent_key = nil)
|
66
|
+
path = []
|
67
|
+
hash.each do |key, value|
|
68
|
+
current_key = parent_key ? "#{parent_key}.#{key}" : key.to_s
|
69
|
+
if value.is_a?(Hash)
|
70
|
+
path += traverse(value, current_key)
|
71
|
+
else
|
72
|
+
path << [current_key, value]
|
73
|
+
end
|
74
|
+
end
|
75
|
+
path
|
76
|
+
end
|
77
|
+
|
78
|
+
def self.hash2keys(hash, parent_key = nil)
|
79
|
+
keys = []
|
80
|
+
hash.each do |key, value|
|
81
|
+
current_key = parent_key ? "#{parent_key}.#{key}" : key.to_s
|
82
|
+
if value.is_a?(Hash)
|
83
|
+
keys += hash2keys(value, current_key)
|
84
|
+
else
|
85
|
+
keys << current_key
|
86
|
+
end
|
87
|
+
end
|
88
|
+
keys
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
@@ -0,0 +1,98 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module LocaleNinja
|
4
|
+
class GithubApiService
|
5
|
+
REPOSITORY_FULLNAME = Rails.application.credentials.github.repository_name
|
6
|
+
private_constant :REPOSITORY_FULLNAME
|
7
|
+
|
8
|
+
def initialize(access_token:)
|
9
|
+
@client = ::Octokit::Client.new(access_token:)
|
10
|
+
end
|
11
|
+
|
12
|
+
attr_reader :client
|
13
|
+
|
14
|
+
def user
|
15
|
+
@client.user
|
16
|
+
end
|
17
|
+
|
18
|
+
def translation_branch(branch)
|
19
|
+
branch.ends_with?('__translations') ? branch : "#{branch}__translations"
|
20
|
+
end
|
21
|
+
|
22
|
+
def locale_files_path(dir = 'config/locales', branch: 'translations')
|
23
|
+
branch = translation_branch(branch) if branch?(translation_branch(branch))
|
24
|
+
@client.contents(repository_fullname, path: dir, ref: "heads/#{branch}").map do |file|
|
25
|
+
if file.type == 'dir'
|
26
|
+
locale_files_path(file.path, branch:)
|
27
|
+
else
|
28
|
+
file.path
|
29
|
+
end
|
30
|
+
end.flatten
|
31
|
+
end
|
32
|
+
|
33
|
+
def pull(files = nil, branch: 'translations')
|
34
|
+
files ||= locale_files_path(branch:)
|
35
|
+
branch = translation_branch(branch) if branch?(translation_branch(branch))
|
36
|
+
|
37
|
+
files.index_with { |path| Base64.decode64(@client.contents(repository_fullname, path:, ref: "heads/#{branch}").content) }
|
38
|
+
end
|
39
|
+
|
40
|
+
def create_translation_branch(branch)
|
41
|
+
return branch if branch.ends_with?('__translations')
|
42
|
+
|
43
|
+
translation_branch = "#{branch}__translations"
|
44
|
+
return translation_branch if branch?(translation_branch)
|
45
|
+
|
46
|
+
create_branch(branch, translation_branch)
|
47
|
+
translation_branch
|
48
|
+
end
|
49
|
+
|
50
|
+
def push(file_path, content, branch: 'translations')
|
51
|
+
branch = create_translation_branch(branch)
|
52
|
+
|
53
|
+
begin
|
54
|
+
sha = @client.content(repository_fullname, path: file_path, ref: "heads/#{branch}")[:sha]
|
55
|
+
rescue ::Octokit::NotFound
|
56
|
+
create_file(file_path, content, branch:)
|
57
|
+
return
|
58
|
+
end
|
59
|
+
@client.update_contents(repository_fullname, file_path, "translations #{DateTime.current}", sha, content, branch:)
|
60
|
+
end
|
61
|
+
|
62
|
+
def create_file(file_path, content, branch: 'translations')
|
63
|
+
@client.create_contents(repository_fullname, file_path, "translations #{DateTime.current}", content, branch:)
|
64
|
+
end
|
65
|
+
|
66
|
+
def create_branch(parent_branch, child_branch)
|
67
|
+
sha = @client.ref(repository_fullname, "heads/#{parent_branch}").dig(:object, :sha)
|
68
|
+
@client.create_ref(repository_fullname, "heads/#{child_branch}", sha)
|
69
|
+
@branches << child_branch
|
70
|
+
end
|
71
|
+
|
72
|
+
def branch?(branch_name)
|
73
|
+
branches.include?(branch_name)
|
74
|
+
end
|
75
|
+
|
76
|
+
def repository_fullname
|
77
|
+
@repository_fullname ||= REPOSITORY_FULLNAME
|
78
|
+
end
|
79
|
+
|
80
|
+
def branches
|
81
|
+
@branches ||= @client.branches(repository_fullname).map(&:name)
|
82
|
+
end
|
83
|
+
|
84
|
+
def default_branch
|
85
|
+
%w[main master].find { |branch_name| branches.include?(branch_name) } || branches.first
|
86
|
+
end
|
87
|
+
|
88
|
+
def public_branches
|
89
|
+
branches.reject { |branch| branch.ends_with?('__translations') }
|
90
|
+
end
|
91
|
+
|
92
|
+
def pull_request(branch_name)
|
93
|
+
@client.create_pull_request(repository_fullname, branch_name, "#{branch_name}__translations", "translations #{Time.current}")
|
94
|
+
rescue ::Octokit::UnprocessableEntity
|
95
|
+
# If pull request already exists, do nothing
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
@@ -0,0 +1,10 @@
|
|
1
|
+
<div class="mb-12 py-4 px-6 rounded-md shadow-md border-solid ">
|
2
|
+
<!-- <%= render partial: "/components/table" %> -->
|
3
|
+
<%= form_with url: branch_select_path, class: "w-full flex justify-between" do |f| %>
|
4
|
+
<div class="flex items-center">
|
5
|
+
<label class="branch-selection text-lg mr-4">Choose a branch:</label>
|
6
|
+
<%= f.select :branch_id, options_for_select(@branches, @branch_name || 'main'), {}, onchange: 'selectionChange(this)', class: "w-80 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block p-2.5" %>
|
7
|
+
</div>
|
8
|
+
<%= f.submit "Fetch this branch", class: "text-white bg-blue-600 px-4 py-3 font-bold rounded cursor-pointer" %>
|
9
|
+
<% end %>
|
10
|
+
</div>
|
@@ -0,0 +1,33 @@
|
|
1
|
+
<div class="fixed bottom-12 right-12 z-20">
|
2
|
+
<% flash.each do |type, message| %>
|
3
|
+
<div class="flex w-full max-w-sm bg-white rounded-md shadow-card my-2 fade-out animated">
|
4
|
+
<div role="alert" class="rounded-xl border border-gray-100 p-4 shadow-xl">
|
5
|
+
<div class="flex items-start gap-4">
|
6
|
+
<% case type %>
|
7
|
+
<% when "notice", "success" %>
|
8
|
+
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6 text-green-600">
|
9
|
+
<path stroke-linecap="round" stroke-linejoin="round" d="M9 12.75L11.25 15 15 9.75M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
10
|
+
</svg>
|
11
|
+
<% when "info" %>
|
12
|
+
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6 text-primary-blue">
|
13
|
+
<path stroke-linecap="round" stroke-linejoin="round" d="M11.25 11.25l.041-.02a.75.75 0 011.063.852l-.708 2.836a.75.75 0 001.063.853l.041-.021M21 12a9 9 0 11-18 0 9 9 0 0118 0zm-9-3.75h.008v.008H12V8.25z" />
|
14
|
+
</svg>
|
15
|
+
<% when "warning" %>
|
16
|
+
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6 text-dark-yellow">
|
17
|
+
<path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126zM12 15.75h.007v.008H12v-.008z" />
|
18
|
+
</svg>
|
19
|
+
<% when "alert", "error" %>
|
20
|
+
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6 text-red-500">
|
21
|
+
<path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m9-.75a9 9 0 11-18 0 9 9 0 0118 0zm-9 3.75h.008v.008H12v-.008z" />
|
22
|
+
</svg>
|
23
|
+
<% else %>
|
24
|
+
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6 text-gray-700">
|
25
|
+
<path stroke-linecap="round" stroke-linejoin="round" d="M11.25 11.25l.041-.02a.75.75 0 011.063.852l-.708 2.836a.75.75 0 001.063.853l.041-.021M21 12a9 9 0 11-18 0 9 9 0 0118 0zm-9-3.75h.008v.008H12V8.25z" />
|
26
|
+
</svg>
|
27
|
+
<% end %>
|
28
|
+
<p class="text-sm text-gray-700"><%= message %></p>
|
29
|
+
</div>
|
30
|
+
</div>
|
31
|
+
</div>
|
32
|
+
<% end %>
|
33
|
+
</div>
|