rosetta-rails 0.1.1 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +165 -9
- data/app/assets/builds/rosetta/application.css +1 -1
- data/app/controllers/concerns/rosetta/locale_scoped.rb +1 -2
- data/app/controllers/rosetta/default_locales_controller.rb +29 -0
- data/app/controllers/rosetta/locales/translations/missing_controller.rb +1 -1
- data/app/controllers/rosetta/locales/translations_controller.rb +2 -2
- data/app/controllers/rosetta/locales_controller.rb +7 -1
- data/app/controllers/rosetta/translations_controller.rb +7 -16
- data/app/helpers/rosetta/translation_helper.rb +1 -1
- data/app/jobs/rosetta/autodiscovery_job.rb +11 -0
- data/app/jobs/rosetta/purge_job.rb +11 -0
- data/app/models/rosetta/locale.rb +18 -10
- data/app/models/rosetta/text_entry.rb +22 -0
- data/app/models/rosetta/translation.rb +11 -2
- data/app/views/rosetta/default_locales/new.html.erb +54 -0
- data/app/views/rosetta/locales/_locale.html.erb +2 -2
- data/app/views/rosetta/locales/translations/_navigation.html.erb +1 -1
- data/app/views/rosetta/locales/translations/index.html.erb +4 -3
- data/app/views/rosetta/{locales/translations/_translation_key.html.erb → text_entries/_text_entry_with_translation.html.erb} +4 -4
- data/app/views/rosetta/translations/edit.html.erb +4 -4
- data/config/routes.rb +3 -1
- data/db/migrate/20240923100651_add_default_to_rosetta_locales.rb +5 -0
- data/db/migrate/20240930135507_create_rosetta_text_entries.rb +14 -0
- data/db/migrate/20240930135810_update_rosetta_translations.rb +20 -0
- data/db/migrate/20241002152043_drop_translation_keys.rb +5 -0
- data/lib/rosetta/configuration.rb +2 -8
- data/lib/rosetta/store.rb +11 -29
- data/lib/rosetta/translated/create.rb +37 -0
- data/lib/rosetta/translated/delete.rb +15 -0
- data/lib/rosetta/translated.rb +55 -0
- data/lib/rosetta/version.rb +1 -1
- data/lib/rosetta-rails.rb +10 -2
- metadata +15 -4
- data/app/models/rosetta/translation_key.rb +0 -7
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 4ccad1f37d3dfef704d60338c2d6e99c0d319c02c1a498c94d6fee302bfb6b95
|
4
|
+
data.tar.gz: 6c40c3391ebcf6cafeba8b1865dc128b416fa878411be608117764c62dcdc3d0
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 3069a77bea26e6a3707d897f1c358d4b74ffbb1331448d79ab132b43541b06e6d28d7f71ccbf034ec12a035ee73d68b34928da4eb0df1331a80e22f8145ec277
|
7
|
+
data.tar.gz: 743d323252d60de6b40980a34b6c107628ad92e0c3bed1638b65b5ac5e7ec45ac5e5556d706dbebede4ec540ae79e6fca203099e7a1128e42d7947a406156d17
|
data/README.md
CHANGED
@@ -1,28 +1,184 @@
|
|
1
|
+
[![CI](https://github.com/virolea/rosetta/actions/workflows/ci.yml/badge.svg)](https://github.com/virolea/rosetta/actions/workflows/ci.yml)
|
2
|
+
|
1
3
|
# Rosetta
|
2
|
-
Short description and motivation.
|
3
4
|
|
4
|
-
|
5
|
-
|
5
|
+
Rosetta is a Rails engine proviving a full-fledged internationalization (i18n) solution for your Rails application. It is designed with the following principles in mind:
|
6
|
+
- **Unobtrusiveness**: i18n matters should not get in your way developing application code.
|
7
|
+
- **Maintanbility**: time should not be spent editing, organizing or deduplicating translation files.
|
8
|
+
- **Separation of concerns**: development work is separated from translation work.
|
9
|
+
|
10
|
+
Rosetta answers those design principles by offering the following features:
|
11
|
+
- **Texts as translation keys**: Inspired by the [gettext](https://www.gnu.org/software/gettext/) approach, Rosetta uses the texts themselves as translation keys. The language the codebase is written into is the default locale.
|
12
|
+
- **Key autodiscovery**: New translation keys are automatically discovered and uniquely saved to the database. No more time spent between views and yaml files.
|
13
|
+
- **A dedicated interface**: Translators can work on a dedicated interface, separated from the codebase. They can search, filter, edit and create translations in a user-friendly environment
|
14
|
+
|
15
|
+
|
16
|
+
A real-life example speaking louder than words, here is a comparison between the Rails default I18n approach and Rosetta:
|
17
|
+
|
18
|
+
```erb
|
19
|
+
<!-- Rails with I18n -->
|
20
|
+
<h1 class="text-2xl font-semibold"><%= t("pages.home.greetings") %> </h1>
|
21
|
+
<p class="text-xl font-medium"><%= t("pages.home.introductory_text") %></p>
|
22
|
+
|
23
|
+
<!-- Rails with Rosetta -->
|
24
|
+
<h1 class="text-2xl font-semibold"><%= _ "Hi! Welcome to Rosetta" %> </h1>
|
25
|
+
<p class="text-xl font-medium">
|
26
|
+
<%= _ "Rosetta is a Rails engine proviving a full-fledged internationalization (i18n) solution for your Rails application." %>
|
27
|
+
</p>
|
28
|
+
```
|
29
|
+
|
30
|
+
## Roadmap
|
31
|
+
|
32
|
+
> [!WARNING]
|
33
|
+
> Rosetta is still early in its development, and is subject to breaking changes even on minor version updates. Please refer to the [releases section](https://github.com/virolea/rosetta/releases) for more information.
|
34
|
+
|
35
|
+
|Feature|tracking|released|
|
36
|
+
|--|--|--
|
37
|
+
|Base translations||0.1.1|
|
38
|
+
|Pluralization API|https://github.com/virolea/rosetta/issues/5||
|
39
|
+
|Context API|https://github.com/virolea/rosetta/issues/6||
|
40
|
+
|Interpolation API|https://github.com/virolea/rosetta/issues/8||
|
6
41
|
|
7
42
|
## Installation
|
8
43
|
Add this line to your application's Gemfile:
|
9
44
|
|
10
45
|
```ruby
|
11
|
-
gem "rosetta"
|
46
|
+
gem "rosetta-rails"
|
12
47
|
```
|
13
48
|
|
14
49
|
And then execute:
|
15
50
|
```bash
|
16
|
-
$ bundle
|
51
|
+
$ bundle install
|
17
52
|
```
|
18
53
|
|
19
|
-
|
54
|
+
Run the following command to install the required migrations:
|
20
55
|
```bash
|
21
|
-
$
|
56
|
+
$ rails rosetta:install:migrations
|
57
|
+
$ rails db:migrate
|
58
|
+
```
|
59
|
+
|
60
|
+
Finally, mount the engine in your `config/routes.rb` file:
|
61
|
+
|
62
|
+
```ruby
|
63
|
+
Rails.application.routes.draw do
|
64
|
+
mount Rosetta::Engine => "/rosetta"
|
65
|
+
end
|
66
|
+
```
|
67
|
+
|
68
|
+
And that's it! Run your server and visit `http://localhost:3000/rosetta` to check everything is running properly.
|
69
|
+
|
70
|
+
## Usage
|
71
|
+
|
72
|
+
### The Default locale
|
73
|
+
|
74
|
+
> [!NOTE]
|
75
|
+
> By convention in Rosetta, the default locale should be the locale your codebase is written into. This, in theory should not change once it is set.
|
76
|
+
|
77
|
+
The first time you visit the Rosetta interface, you will be prompted to set the default locale:
|
78
|
+
|
79
|
+
![CleanShot 2024-09-23 at 15 36 40](https://github.com/user-attachments/assets/6d088837-89ac-4742-b5b7-00a7db8f9c2b)
|
80
|
+
|
81
|
+
Enter the name and the code for the locale your codebase is written into (probably English and `en` in most cases) and submit the form. You will be redirected to the rosetta homepage where your default locale will appear.
|
82
|
+
|
83
|
+
![CleanShot 2024-09-19 at 18 01 55](https://github.com/user-attachments/assets/678fad1d-64cd-463d-8c21-9a166abca715)
|
84
|
+
|
85
|
+
### Adding a locale
|
86
|
+
|
87
|
+
Developers shout not worry about managing locales. Locales can be added through the Rosetta interface. Visit the interface and click on the "Add locale" button. You will be prompted to enter the name and code of the locale. Once added, the locale will immediately be available and ready to be translated.
|
88
|
+
|
89
|
+
![CleanShot 2024-09-19 at 15 11 21](https://github.com/user-attachments/assets/ffe55e81-5cee-4b5b-9be1-a38abe124dfe)
|
90
|
+
|
91
|
+
### Selecting the locale in the application
|
92
|
+
|
93
|
+
The current locale is globally available in the current thread the application is running in. You can access it through the `Rosetta.locale` method. This will return a `Rosetta::Locale` instance.
|
94
|
+
|
95
|
+
It is recommended to follow the [Rails guides best practices](https://guides.rubyonrails.org/i18n.html#managing-the-locale-across-requests) to manage the locale across requests. Rosetta offers a similar helper method to set the locale for the current request:
|
96
|
+
|
97
|
+
```ruby
|
98
|
+
class ApplicationController < ActionController::Base
|
99
|
+
around_action :set_locale
|
100
|
+
|
101
|
+
private
|
102
|
+
|
103
|
+
def set_locale(&action)
|
104
|
+
Rosetta.with_locale(params[:locale], &action)
|
105
|
+
end
|
22
106
|
```
|
23
107
|
|
24
|
-
|
25
|
-
|
108
|
+
Where locale is the code of the locale you want to set. You can then set the locale by passing the locale code as a query parameter in the URL, like `http://localhost:3000?locale=fr`. If no locale exists for this code, the default locale is used instead.
|
109
|
+
|
110
|
+
You can access available locales through `Rosetta.available_locales` to build a selector your users can use to swith between locales:
|
111
|
+
|
112
|
+
```erb
|
113
|
+
<ul>
|
114
|
+
<% Rosetta.available_locales.each do |locale| %>
|
115
|
+
<li><%= link_to locale.name, root_path(locale: locale.code) %></li>
|
116
|
+
<% end %>
|
117
|
+
</ul>
|
118
|
+
```
|
119
|
+
|
120
|
+
### Translating your views
|
121
|
+
|
122
|
+
> [!IMPORTANT]
|
123
|
+
> Make sure you've correctly installed rosetta, and that your production environment has run the rosetta migrations properly before invoking the `_` helper in your views.
|
124
|
+
|
125
|
+
As stated above, Rosetta is built with unobstrusiveness in mind. To translate your view, no need to come up with a translation key, figure out the file and appropriate nesting to write to. All you need to do is include the `Rosetta::TranslationHelper` in your `ApplicationHelper` and wrap your text with the `_` method in your view:
|
126
|
+
|
127
|
+
```ruby
|
128
|
+
module ApplicationHelper
|
129
|
+
include Rosetta::TranslationHelper
|
130
|
+
end
|
131
|
+
```
|
132
|
+
|
133
|
+
```erb
|
134
|
+
<h1"><%= _ "Hi! Welcome to Rosetta" %> </h1>
|
135
|
+
```
|
136
|
+
|
137
|
+
Rosetta will automatically detect the new translation keys and save them to the database. You can then visit the Rosetta interface to add a translation for a given locale.
|
138
|
+
|
139
|
+
### Adding translations
|
140
|
+
|
141
|
+
Translations can be added through the Rosetta interface. Visit the interface and click on "Manage" button next to the locale you want to add translations for. You will access the list of your app keys, and the corresponding translations, should they exist. To add or edit a translation, hover the translation cell and click on "edit". You can then enter the translation and save it.
|
142
|
+
|
143
|
+
![CleanShot 2024-09-19 at 15 14 32](https://github.com/user-attachments/assets/d6d57b0a-034c-409e-8ff1-bfcae84aa1c8)
|
144
|
+
|
145
|
+
|
146
|
+
Saving the new translation **will not immediately reflect in your application**. This is by desgin for performance reasons. To make new translations available in your app, click on the "deploy" button at the top of the page. Translations for the given locale will be reloaded and made available in your app.
|
147
|
+
|
148
|
+
![CleanShot 2024-09-19 at 15 15 30](https://github.com/user-attachments/assets/51a9b582-b35e-4df4-a79b-1e0f238715a0)
|
149
|
+
|
150
|
+
|
151
|
+
### Configuration
|
152
|
+
|
153
|
+
#### Configuring a base controller
|
154
|
+
|
155
|
+
By default, Rosetta inherits from `ActionController::Base`. If you want to restrict access to the Rosetta interface, you can make Rosetta inherit from your own base controller, by configuring it in the initializer:
|
156
|
+
|
157
|
+
```ruby
|
158
|
+
# config/initializers/rosetta.rb
|
159
|
+
|
160
|
+
Rosetta.configure do |config|
|
161
|
+
config.parent_controller_class = "AdminController"
|
162
|
+
end
|
163
|
+
```
|
164
|
+
|
165
|
+
**Note**: The class name needs to be a string.
|
166
|
+
|
167
|
+
#### Setting up the queue for the Autodiscovery Job
|
168
|
+
|
169
|
+
Rosetta uses a background job to automatically discover new translations in your codebase. It enqueues jobs in the queue set by default. To configure it to target another queue:
|
170
|
+
|
171
|
+
```ruby
|
172
|
+
# config/initializers/rosetta.rb
|
173
|
+
|
174
|
+
Rosetta.configure do |config|
|
175
|
+
config.queues[:autodiscovery] = "low_priority"
|
176
|
+
end
|
177
|
+
```
|
178
|
+
|
179
|
+
### I18n Support
|
180
|
+
|
181
|
+
As of now, Rosetta does not integrate with the `i18n` gem as a custom backend. It might be done in the future, however it's been decided to build Rosetta independently for now. You still need to use `i18n` for backwards compatibility of your existing translations, localization, as well as the translations of gems that depend on it.
|
26
182
|
|
27
183
|
## License
|
28
184
|
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
@@ -1 +1 @@
|
|
1
|
-
/*! tailwindcss v3.4.10 | MIT License | https://tailwindcss.com*/*,:after,:before{box-sizing:border-box;border:0 solid #e5e7eb}:after,:before{--tw-content:""}:host,html{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}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-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-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{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}[role=button],button{cursor:pointer}:disabled{cursor:default}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[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],input:where(:not([type])),select,textarea{-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:#fff;border-color:#6b7280;border-width:1px;border-radius:0;padding:.5rem .75rem;font-size:1rem;line-height:1.5rem;--tw-shadow:0 0 #0000}[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,input:where(:not([type])):focus,select:focus,textarea:focus{outline:2px solid transparent;outline-offset:2px;--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);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);border-color:#2563eb}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;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-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-top:0;padding-bottom: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],[size]:where(select:not([size="1"])){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]{-webkit-appearance:none;-moz-appearance:none;appearance:none;padding:0;-webkit-print-color-adjust:exact;print-color-adjust:exact;display:inline-block;vertical-align:middle;background-origin:border-box;-webkit-user-select:none;-moz-user-select:none;user-select:none;flex-shrink:0;height:1rem;width:1rem;color:#2563eb;background-color:#fff;border-color:#6b7280;border-width:1px;--tw-shadow:0 0 #0000}[type=checkbox]{border-radius:0}[type=radio]{border-radius:100%}[type=checkbox]:focus,[type=radio]:focus{outline:2px solid transparent;outline-offset:2px;--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)}[type=checkbox]:checked,[type=radio]:checked{border-color:transparent;background-color:currentColor;background-size:100% 100%;background-position:50%;background-repeat:no-repeat}[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 0'/%3E%3C/svg%3E")}@media (forced-colors:active) {[type=checkbox]:checked{-webkit-appearance:auto;-moz-appearance:auto;appearance:auto}}[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")}@media (forced-colors:active) {[type=radio]:checked{-webkit-appearance:auto;-moz-appearance:auto;appearance:auto}}[type=checkbox]:checked:focus,[type=checkbox]:checked:hover,[type=radio]:checked:focus,[type=radio]:checked:hover{border-color:transparent;background-color:currentColor}[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");border-color:transparent;background-color:currentColor;background-size:100% 100%;background-position:50%;background-repeat:no-repeat}@media (forced-colors:active) {[type=checkbox]:indeterminate{-webkit-appearance:auto;-moz-appearance:auto;appearance:auto}}[type=checkbox]:indeterminate:focus,[type=checkbox]:indeterminate:hover{border-color:transparent;background-color:currentColor}[type=file]{background:unset;border-color:inherit;border-width:0;border-radius:0;padding:0;font-size:unset;line-height:inherit}[type=file]:focus{outline:1px solid ButtonText;outline:1px auto -webkit-focus-ring-color}*,: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:rgba(59,130,246,.5);--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: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }::backdrop{--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:rgba(59,130,246,.5);--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: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }.btn{border-radius:.375rem;padding:.5rem .75rem;font-size:.875rem;line-height:1.25rem;font-weight:600;--tw-shadow:0 1px 2px 0 rgba(0,0,0,.05);--tw-shadow-colored:0 1px 2px 0 var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.btn:focus-visible{outline-style:solid;outline-width:2px;outline-offset:2px}.btn-primary{--tw-bg-opacity:1;background-color:rgb(79 70 229/var(--tw-bg-opacity));--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity))}.btn-primary:hover{--tw-bg-opacity:1;background-color:rgb(99 102 241/var(--tw-bg-opacity))}.btn-primary:focus-visible{outline-color:#4f46e5}.btn-secondary{--tw-bg-opacity:1;background-color:rgb(255 255 255/var(--tw-bg-opacity));--tw-text-opacity:1;color:rgb(17 24 39/var(--tw-text-opacity));--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);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000);--tw-ring-inset:inset;--tw-ring-opacity:1;--tw-ring-color:rgb(209 213 219/var(--tw-ring-opacity))}.btn-secondary:hover{--tw-bg-opacity:1;background-color:rgb(249 250 251/var(--tw-bg-opacity))}.label{font-size:.875rem;font-weight:500;line-height:1.5rem}.input,.label{--tw-text-opacity:1;color:rgb(17 24 39/var(--tw-text-opacity))}.input{display:block;width:100%;border-radius:.375rem;border-width:0;padding-top:.375rem;padding-bottom:.375rem;--tw-shadow:0 1px 2px 0 rgba(0,0,0,.05);--tw-shadow-colored:0 1px 2px 0 var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow);--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);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000);--tw-ring-inset:inset;--tw-ring-opacity:1;--tw-ring-color:rgb(209 213 219/var(--tw-ring-opacity))}.input::-moz-placeholder{--tw-text-opacity:1;color:rgb(156 163 175/var(--tw-text-opacity))}.input::placeholder{--tw-text-opacity:1;color:rgb(156 163 175/var(--tw-text-opacity))}.input: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);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000);--tw-ring-inset:inset;--tw-ring-opacity:1;--tw-ring-color:rgb(79 70 229/var(--tw-ring-opacity))}@media (min-width:640px){.input{font-size:.875rem;line-height:1.5rem}}.badge{border-radius:.375rem;--tw-bg-opacity:1;background-color:rgb(249 250 251/var(--tw-bg-opacity));--tw-text-opacity:1;color:rgb(75 85 99/var(--tw-text-opacity));--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);--tw-ring-inset:inset;--tw-ring-color:hsla(220,9%,46%,.1)}.badge,.pill{display:inline-flex;align-items:center;padding:.25rem .5rem;font-size:.75rem;line-height:1rem;font-weight:500;box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000)}.pill{border-radius:9999px;--tw-bg-opacity:1;background-color:rgb(240 253 244/var(--tw-bg-opacity));--tw-text-opacity:1;color:rgb(21 128 61/var(--tw-text-opacity));--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);--tw-ring-inset:inset;--tw-ring-color:rgba(22,163,74,.2)}.pagy{display:flex;justify-content:center}.pagy>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-right:calc(.25rem*var(--tw-space-x-reverse));margin-left:calc(.25rem*(1 - var(--tw-space-x-reverse)))}.pagy{font-size:.875rem;line-height:1.25rem;font-weight:600;--tw-text-opacity:1;color:rgb(107 114 128/var(--tw-text-opacity));a:not(.gap){display:block;border-radius:.5rem;--tw-bg-opacity:1;background-color:rgb(229 231 235/var(--tw-bg-opacity));padding-left:.75rem;padding-right:.75rem;padding-top:.25rem;padding-bottom:.25rem}a:not(.gap){&:hover{--tw-bg-opacity:1;background-color:rgb(209 213 219/var(--tw-bg-opacity))}&:not([href]){cursor:default;--tw-bg-opacity:1;background-color:rgb(243 244 246/var(--tw-bg-opacity));--tw-text-opacity:1;color:rgb(209 213 219/var(--tw-text-opacity))}&.current{--tw-bg-opacity:1;background-color:rgb(79 70 229/var(--tw-bg-opacity));--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity))}}label{display:inline-block;white-space:nowrap;border-radius:.5rem;--tw-bg-opacity:1;background-color:rgb(229 231 235/var(--tw-bg-opacity));padding-left:.75rem;padding-right:.75rem;padding-top:.125rem;padding-bottom:.125rem}label{input{border-radius:.375rem;border-style:none;--tw-bg-opacity:1;background-color:rgb(243 244 246/var(--tw-bg-opacity))}}}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border-width:0}.static{position:static}.absolute{position:absolute}.relative{position:relative}.inset-x-0{left:0;right:0}.bottom-0{bottom:0}.left-0{left:0}.top-0{top:0}.top-4{top:1rem}.m-0{margin:0}.mx-auto{margin-left:auto;margin-right:auto}.-mb-px{margin-bottom:-1px}.ml-2{margin-left:.5rem}.ml-3{margin-left:.75rem}.mt-1{margin-top:.25rem}.mt-2{margin-top:.5rem}.mt-4{margin-top:1rem}.mt-6{margin-top:1.5rem}.mt-8{margin-top:2rem}.block{display:block}.inline{display:inline}.flex{display:flex}.table{display:table}.grid{display:grid}.hidden{display:none}.h-5{height:1.25rem}.h-6{height:1.5rem}.h-9{height:2.25rem}.h-full{height:100%}.min-h-full{min-height:100%}.w-1\/2{width:50%}.w-5{width:1.25rem}.w-6{width:1.5rem}.w-full{width:100%}.min-w-full{min-width:100%}.max-w-28{max-width:7rem}.max-w-4xl{max-width:56rem}.max-w-xl{max-width:36rem}.max-w-xs{max-width:20rem}.flex-shrink-0{flex-shrink:0}@keyframes spin{to{transform:rotate(1turn)}}.animate-spin{animation:spin 1s linear infinite}.resize-none{resize:none}.items-center{align-items:center}.justify-end{justify-content:flex-end}.justify-between{justify-content:space-between}.justify-around{justify-content:space-around}.gap-2{gap:.5rem}.space-x-2>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-right:calc(.5rem*var(--tw-space-x-reverse));margin-left:calc(.5rem*(1 - var(--tw-space-x-reverse)))}.space-x-8>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-right:calc(2rem*var(--tw-space-x-reverse));margin-left:calc(2rem*(1 - var(--tw-space-x-reverse)))}.divide-x>:not([hidden])~:not([hidden]){--tw-divide-x-reverse:0;border-right-width:calc(1px*var(--tw-divide-x-reverse));border-left-width:calc(1px*(1 - var(--tw-divide-x-reverse)))}.divide-y>:not([hidden])~:not([hidden]){--tw-divide-y-reverse:0;border-top-width:calc(1px*(1 - var(--tw-divide-y-reverse)));border-bottom-width:calc(1px*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))}.divide-gray-300>:not([hidden])~:not([hidden]){--tw-divide-opacity:1;border-color:rgb(209 213 219/var(--tw-divide-opacity))}.overflow-hidden{overflow:hidden}.whitespace-nowrap{white-space:nowrap}.rounded-full{border-radius:9999px}.rounded-lg{border-radius:.5rem}.rounded-md{border-radius:.375rem}.border-0{border-width:0}.border-b{border-bottom-width:1px}.border-b-2{border-bottom-width:2px}.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-green-100{--tw-border-opacity:1;border-color:rgb(220 252 231/var(--tw-border-opacity))}.border-indigo-500{--tw-border-opacity:1;border-color:rgb(99 102 241/var(--tw-border-opacity))}.border-transparent{border-color:transparent}.bg-gray-100{--tw-bg-opacity:1;background-color:rgb(243 244 246/var(--tw-bg-opacity))}.bg-gray-50{--tw-bg-opacity:1;background-color:rgb(249 250 251/var(--tw-bg-opacity))}.bg-green-50{--tw-bg-opacity:1;background-color:rgb(240 253 244/var(--tw-bg-opacity))}.bg-indigo-50{--tw-bg-opacity:1;background-color:rgb(238 242 255/var(--tw-bg-opacity))}.bg-red-50{--tw-bg-opacity:1;background-color:rgb(254 242 242/var(--tw-bg-opacity))}.bg-transparent{background-color:transparent}.bg-white{--tw-bg-opacity:1;background-color:rgb(255 255 255/var(--tw-bg-opacity))}.bg-opacity-50{--tw-bg-opacity:0.5}.p-12{padding:3rem}.p-4{padding:1rem}.px-1{padding-left:.25rem;padding-right:.25rem}.px-2\.5{padding-left:.625rem;padding-right:.625rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-4{padding-left:1rem;padding-right:1rem}.px-6{padding-left:1.5rem;padding-right:1.5rem}.py-0\.5{padding-top:.125rem;padding-bottom:.125rem}.py-1\.5{padding-top:.375rem;padding-bottom:.375rem}.py-10{padding-top:2.5rem;padding-bottom:2.5rem}.py-12{padding-top:3rem;padding-bottom:3rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.py-3\.5{padding-top:.875rem;padding-bottom:.875rem}.py-4{padding-top:1rem;padding-bottom:1rem}.py-px{padding-top:1px;padding-bottom:1px}.pl-3{padding-left:.75rem}.pl-4{padding-left:1rem}.pr-2{padding-right:.5rem}.pr-3{padding-right:.75rem}.pr-4{padding-right:1rem}.text-left{text-align:left}.text-right{text-align:right}.text-base{font-size:1rem;line-height:1.5rem}.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-medium{font-weight:500}.font-semibold{font-weight:600}.uppercase{text-transform:uppercase}.leading-6{line-height:1.5rem}.text-black{--tw-text-opacity:1;color:rgb(0 0 0/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-900{--tw-text-opacity:1;color:rgb(17 24 39/var(--tw-text-opacity))}.text-green-800{--tw-text-opacity:1;color:rgb(22 101 52/var(--tw-text-opacity))}.text-indigo-600{--tw-text-opacity:1;color:rgb(79 70 229/var(--tw-text-opacity))}.text-red-400{--tw-text-opacity:1;color:rgb(248 113 113/var(--tw-text-opacity))}.text-red-700{--tw-text-opacity:1;color:rgb(185 28 28/var(--tw-text-opacity))}.opacity-25{opacity:.25}.opacity-75{opacity:.75}.shadow{--tw-shadow:0 1px 3px 0 rgba(0,0,0,.1),0 1px 2px -1px rgba(0,0,0,.1);--tw-shadow-colored:0 1px 3px 0 var(--tw-shadow-color),0 1px 2px -1px var(--tw-shadow-color)}.shadow,.shadow-sm{box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.shadow-sm{--tw-shadow:0 1px 2px 0 rgba(0,0,0,.05);--tw-shadow-colored:0 1px 2px 0 var(--tw-shadow-color)}.ring-1{--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);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000)}.ring-inset{--tw-ring-inset:inset}.ring-black{--tw-ring-opacity:1;--tw-ring-color:rgb(0 0 0/var(--tw-ring-opacity))}.ring-gray-300{--tw-ring-opacity:1;--tw-ring-color:rgb(209 213 219/var(--tw-ring-opacity))}.ring-opacity-5{--tw-ring-opacity:0.05}.placeholder\:text-gray-400::-moz-placeholder{--tw-text-opacity:1;color:rgb(156 163 175/var(--tw-text-opacity))}.placeholder\:text-gray-400::placeholder{--tw-text-opacity:1;color:rgb(156 163 175/var(--tw-text-opacity))}.focus-within\:ring-2:focus-within{--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,0 0 #0000)}.focus-within\:ring-indigo-600:focus-within{--tw-ring-opacity:1;--tw-ring-color:rgb(79 70 229/var(--tw-ring-opacity))}.hover\:border-gray-200:hover{--tw-border-opacity:1;border-color:rgb(229 231 235/var(--tw-border-opacity))}.hover\:text-gray-500:hover{--tw-text-opacity:1;color:rgb(107 114 128/var(--tw-text-opacity))}.hover\:text-gray-700:hover{--tw-text-opacity:1;color:rgb(55 65 81/var(--tw-text-opacity))}.hover\:text-indigo-900:hover{--tw-text-opacity:1;color:rgb(49 46 129/var(--tw-text-opacity))}.focus\:outline-none:focus{outline:2px solid transparent;outline-offset:2px}.focus\:ring-0: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(var(--tw-ring-offset-width)) var(--tw-ring-color)}.focus\:ring-0:focus,.focus\:ring-2: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-indigo-500:focus{--tw-ring-opacity:1;--tw-ring-color:rgb(99 102 241/var(--tw-ring-opacity))}.focus\:ring-offset-2:focus{--tw-ring-offset-width:2px}.group:hover .group-hover\:flex{display:flex}.group.active .group-\[\.active\]\:bg-indigo-100{--tw-bg-opacity:1;background-color:rgb(224 231 255/var(--tw-bg-opacity))}.group.active .group-\[\.active\]\:text-indigo-600{--tw-text-opacity:1;color:rgb(79 70 229/var(--tw-text-opacity))}@media (min-width:640px){.sm\:ml-16{margin-left:4rem}.sm\:mt-0{margin-top:0}.sm\:flex{display:flex}.sm\:flex-auto{flex:1 1 auto}.sm\:flex-none{flex:none}.sm\:items-center{align-items:center}.sm\:rounded-lg{border-radius:.5rem}.sm\:px-6{padding-right:1.5rem}.sm\:pl-6,.sm\:px-6{padding-left:1.5rem}.sm\:pr-6{padding-right:1.5rem}.sm\:text-sm{font-size:.875rem;line-height:1.25rem}.sm\:leading-6{line-height:1.5rem}}@media (min-width:768px){.md\:inline-block{display:inline-block}}@media (min-width:1024px){.lg\:px-8{padding-left:2rem;padding-right:2rem}}
|
1
|
+
/*! tailwindcss v3.4.10 | MIT License | https://tailwindcss.com*/*,:after,:before{box-sizing:border-box;border:0 solid #e5e7eb}:after,:before{--tw-content:""}:host,html{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}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-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-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{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}[role=button],button{cursor:pointer}:disabled{cursor:default}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[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],input:where(:not([type])),select,textarea{-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:#fff;border-color:#6b7280;border-width:1px;border-radius:0;padding:.5rem .75rem;font-size:1rem;line-height:1.5rem;--tw-shadow:0 0 #0000}[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,input:where(:not([type])):focus,select:focus,textarea:focus{outline:2px solid transparent;outline-offset:2px;--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);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);border-color:#2563eb}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;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-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-top:0;padding-bottom: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],[size]:where(select:not([size="1"])){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]{-webkit-appearance:none;-moz-appearance:none;appearance:none;padding:0;-webkit-print-color-adjust:exact;print-color-adjust:exact;display:inline-block;vertical-align:middle;background-origin:border-box;-webkit-user-select:none;-moz-user-select:none;user-select:none;flex-shrink:0;height:1rem;width:1rem;color:#2563eb;background-color:#fff;border-color:#6b7280;border-width:1px;--tw-shadow:0 0 #0000}[type=checkbox]{border-radius:0}[type=radio]{border-radius:100%}[type=checkbox]:focus,[type=radio]:focus{outline:2px solid transparent;outline-offset:2px;--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)}[type=checkbox]:checked,[type=radio]:checked{border-color:transparent;background-color:currentColor;background-size:100% 100%;background-position:50%;background-repeat:no-repeat}[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 0'/%3E%3C/svg%3E")}@media (forced-colors:active) {[type=checkbox]:checked{-webkit-appearance:auto;-moz-appearance:auto;appearance:auto}}[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")}@media (forced-colors:active) {[type=radio]:checked{-webkit-appearance:auto;-moz-appearance:auto;appearance:auto}}[type=checkbox]:checked:focus,[type=checkbox]:checked:hover,[type=radio]:checked:focus,[type=radio]:checked:hover{border-color:transparent;background-color:currentColor}[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");border-color:transparent;background-color:currentColor;background-size:100% 100%;background-position:50%;background-repeat:no-repeat}@media (forced-colors:active) {[type=checkbox]:indeterminate{-webkit-appearance:auto;-moz-appearance:auto;appearance:auto}}[type=checkbox]:indeterminate:focus,[type=checkbox]:indeterminate:hover{border-color:transparent;background-color:currentColor}[type=file]{background:unset;border-color:inherit;border-width:0;border-radius:0;padding:0;font-size:unset;line-height:inherit}[type=file]:focus{outline:1px solid ButtonText;outline:1px auto -webkit-focus-ring-color}*,: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:rgba(59,130,246,.5);--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: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }::backdrop{--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:rgba(59,130,246,.5);--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: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }.btn{border-radius:.375rem;padding:.5rem .75rem;font-size:.875rem;line-height:1.25rem;font-weight:600;--tw-shadow:0 1px 2px 0 rgba(0,0,0,.05);--tw-shadow-colored:0 1px 2px 0 var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.btn:focus-visible{outline-style:solid;outline-width:2px;outline-offset:2px}.btn-primary{--tw-bg-opacity:1;background-color:rgb(79 70 229/var(--tw-bg-opacity));--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity))}.btn-primary:hover{--tw-bg-opacity:1;background-color:rgb(99 102 241/var(--tw-bg-opacity))}.btn-primary:focus-visible{outline-color:#4f46e5}.btn-secondary{--tw-bg-opacity:1;background-color:rgb(255 255 255/var(--tw-bg-opacity));--tw-text-opacity:1;color:rgb(17 24 39/var(--tw-text-opacity));--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);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000);--tw-ring-inset:inset;--tw-ring-opacity:1;--tw-ring-color:rgb(209 213 219/var(--tw-ring-opacity))}.btn-secondary:hover{--tw-bg-opacity:1;background-color:rgb(249 250 251/var(--tw-bg-opacity))}.label{font-size:.875rem;font-weight:500;line-height:1.5rem}.input,.label{--tw-text-opacity:1;color:rgb(17 24 39/var(--tw-text-opacity))}.input{display:block;width:100%;border-radius:.375rem;border-width:0;padding-top:.375rem;padding-bottom:.375rem;--tw-shadow:0 1px 2px 0 rgba(0,0,0,.05);--tw-shadow-colored:0 1px 2px 0 var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow);--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);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000);--tw-ring-inset:inset;--tw-ring-opacity:1;--tw-ring-color:rgb(209 213 219/var(--tw-ring-opacity))}.input::-moz-placeholder{--tw-text-opacity:1;color:rgb(156 163 175/var(--tw-text-opacity))}.input::placeholder{--tw-text-opacity:1;color:rgb(156 163 175/var(--tw-text-opacity))}.input: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);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000);--tw-ring-inset:inset;--tw-ring-opacity:1;--tw-ring-color:rgb(79 70 229/var(--tw-ring-opacity))}@media (min-width:640px){.input{font-size:.875rem;line-height:1.5rem}}.badge{border-radius:.375rem;--tw-bg-opacity:1;background-color:rgb(249 250 251/var(--tw-bg-opacity));--tw-text-opacity:1;color:rgb(75 85 99/var(--tw-text-opacity));--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);--tw-ring-inset:inset;--tw-ring-color:hsla(220,9%,46%,.1)}.badge,.pill{display:inline-flex;align-items:center;padding:.25rem .5rem;font-size:.75rem;line-height:1rem;font-weight:500;box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000)}.pill{border-radius:9999px;--tw-bg-opacity:1;background-color:rgb(240 253 244/var(--tw-bg-opacity));--tw-text-opacity:1;color:rgb(21 128 61/var(--tw-text-opacity));--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);--tw-ring-inset:inset;--tw-ring-color:rgba(22,163,74,.2)}.pagy{display:flex;justify-content:center}.pagy>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-right:calc(.25rem*var(--tw-space-x-reverse));margin-left:calc(.25rem*(1 - var(--tw-space-x-reverse)))}.pagy{font-size:.875rem;line-height:1.25rem;font-weight:600;--tw-text-opacity:1;color:rgb(107 114 128/var(--tw-text-opacity));a:not(.gap){display:block;border-radius:.5rem;--tw-bg-opacity:1;background-color:rgb(229 231 235/var(--tw-bg-opacity));padding-left:.75rem;padding-right:.75rem;padding-top:.25rem;padding-bottom:.25rem}a:not(.gap){&:hover{--tw-bg-opacity:1;background-color:rgb(209 213 219/var(--tw-bg-opacity))}&:not([href]){cursor:default;--tw-bg-opacity:1;background-color:rgb(243 244 246/var(--tw-bg-opacity));--tw-text-opacity:1;color:rgb(209 213 219/var(--tw-text-opacity))}&.current{--tw-bg-opacity:1;background-color:rgb(79 70 229/var(--tw-bg-opacity));--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity))}}label{display:inline-block;white-space:nowrap;border-radius:.5rem;--tw-bg-opacity:1;background-color:rgb(229 231 235/var(--tw-bg-opacity));padding-left:.75rem;padding-right:.75rem;padding-top:.125rem;padding-bottom:.125rem}label{input{border-radius:.375rem;border-style:none;--tw-bg-opacity:1;background-color:rgb(243 244 246/var(--tw-bg-opacity))}}}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border-width:0}.static{position:static}.absolute{position:absolute}.relative{position:relative}.inset-x-0{left:0;right:0}.bottom-0{bottom:0}.left-0{left:0}.top-0{top:0}.top-4{top:1rem}.m-0{margin:0}.mx-auto{margin-left:auto;margin-right:auto}.-mb-px{margin-bottom:-1px}.ml-2{margin-left:.5rem}.ml-3{margin-left:.75rem}.mt-1{margin-top:.25rem}.mt-2{margin-top:.5rem}.mt-4{margin-top:1rem}.mt-6{margin-top:1.5rem}.mt-8{margin-top:2rem}.block{display:block}.inline{display:inline}.flex{display:flex}.table{display:table}.grid{display:grid}.hidden{display:none}.h-5{height:1.25rem}.h-6{height:1.5rem}.h-9{height:2.25rem}.h-full{height:100%}.min-h-full{min-height:100%}.w-1\/2{width:50%}.w-5{width:1.25rem}.w-6{width:1.5rem}.w-full{width:100%}.min-w-full{min-width:100%}.max-w-28{max-width:7rem}.max-w-4xl{max-width:56rem}.max-w-xl{max-width:36rem}.max-w-xs{max-width:20rem}.flex-shrink-0{flex-shrink:0}@keyframes spin{to{transform:rotate(1turn)}}.animate-spin{animation:spin 1s linear infinite}.resize-none{resize:none}.items-center{align-items:center}.justify-end{justify-content:flex-end}.justify-between{justify-content:space-between}.justify-around{justify-content:space-around}.gap-2{gap:.5rem}.space-x-2>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-right:calc(.5rem*var(--tw-space-x-reverse));margin-left:calc(.5rem*(1 - var(--tw-space-x-reverse)))}.space-x-8>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-right:calc(2rem*var(--tw-space-x-reverse));margin-left:calc(2rem*(1 - var(--tw-space-x-reverse)))}.divide-x>:not([hidden])~:not([hidden]){--tw-divide-x-reverse:0;border-right-width:calc(1px*var(--tw-divide-x-reverse));border-left-width:calc(1px*(1 - var(--tw-divide-x-reverse)))}.divide-y>:not([hidden])~:not([hidden]){--tw-divide-y-reverse:0;border-top-width:calc(1px*(1 - var(--tw-divide-y-reverse)));border-bottom-width:calc(1px*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))}.divide-gray-300>:not([hidden])~:not([hidden]){--tw-divide-opacity:1;border-color:rgb(209 213 219/var(--tw-divide-opacity))}.overflow-hidden{overflow:hidden}.whitespace-nowrap{white-space:nowrap}.rounded-full{border-radius:9999px}.rounded-lg{border-radius:.5rem}.rounded-md{border-radius:.375rem}.border-0{border-width:0}.border-b{border-bottom-width:1px}.border-b-2{border-bottom-width:2px}.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-green-100{--tw-border-opacity:1;border-color:rgb(220 252 231/var(--tw-border-opacity))}.border-indigo-500{--tw-border-opacity:1;border-color:rgb(99 102 241/var(--tw-border-opacity))}.border-transparent{border-color:transparent}.bg-blue-50{--tw-bg-opacity:1;background-color:rgb(239 246 255/var(--tw-bg-opacity))}.bg-gray-100{--tw-bg-opacity:1;background-color:rgb(243 244 246/var(--tw-bg-opacity))}.bg-gray-50{--tw-bg-opacity:1;background-color:rgb(249 250 251/var(--tw-bg-opacity))}.bg-green-50{--tw-bg-opacity:1;background-color:rgb(240 253 244/var(--tw-bg-opacity))}.bg-indigo-50{--tw-bg-opacity:1;background-color:rgb(238 242 255/var(--tw-bg-opacity))}.bg-red-50{--tw-bg-opacity:1;background-color:rgb(254 242 242/var(--tw-bg-opacity))}.bg-transparent{background-color:transparent}.bg-white{--tw-bg-opacity:1;background-color:rgb(255 255 255/var(--tw-bg-opacity))}.bg-opacity-50{--tw-bg-opacity:0.5}.p-12{padding:3rem}.p-4{padding:1rem}.px-1{padding-left:.25rem;padding-right:.25rem}.px-2\.5{padding-left:.625rem;padding-right:.625rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-4{padding-left:1rem;padding-right:1rem}.px-6{padding-left:1.5rem;padding-right:1.5rem}.py-0\.5{padding-top:.125rem;padding-bottom:.125rem}.py-1\.5{padding-top:.375rem;padding-bottom:.375rem}.py-10{padding-top:2.5rem;padding-bottom:2.5rem}.py-12{padding-top:3rem;padding-bottom:3rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.py-3\.5{padding-top:.875rem;padding-bottom:.875rem}.py-4{padding-top:1rem;padding-bottom:1rem}.py-px{padding-top:1px;padding-bottom:1px}.pl-3{padding-left:.75rem}.pl-4{padding-left:1rem}.pr-2{padding-right:.5rem}.pr-3{padding-right:.75rem}.pr-4{padding-right:1rem}.text-left{text-align:left}.text-right{text-align:right}.text-3xl{font-size:1.875rem;line-height:2.25rem}.text-base{font-size:1rem;line-height:1.5rem}.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-medium{font-weight:500}.font-semibold{font-weight:600}.uppercase{text-transform:uppercase}.leading-6{line-height:1.5rem}.text-black{--tw-text-opacity:1;color:rgb(0 0 0/var(--tw-text-opacity))}.text-blue-400{--tw-text-opacity:1;color:rgb(96 165 250/var(--tw-text-opacity))}.text-blue-700{--tw-text-opacity:1;color:rgb(29 78 216/var(--tw-text-opacity))}.text-blue-800{--tw-text-opacity:1;color:rgb(30 64 175/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-900{--tw-text-opacity:1;color:rgb(17 24 39/var(--tw-text-opacity))}.text-green-800{--tw-text-opacity:1;color:rgb(22 101 52/var(--tw-text-opacity))}.text-indigo-600{--tw-text-opacity:1;color:rgb(79 70 229/var(--tw-text-opacity))}.text-red-400{--tw-text-opacity:1;color:rgb(248 113 113/var(--tw-text-opacity))}.text-red-700{--tw-text-opacity:1;color:rgb(185 28 28/var(--tw-text-opacity))}.opacity-25{opacity:.25}.opacity-75{opacity:.75}.shadow{--tw-shadow:0 1px 3px 0 rgba(0,0,0,.1),0 1px 2px -1px rgba(0,0,0,.1);--tw-shadow-colored:0 1px 3px 0 var(--tw-shadow-color),0 1px 2px -1px var(--tw-shadow-color)}.shadow,.shadow-sm{box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.shadow-sm{--tw-shadow:0 1px 2px 0 rgba(0,0,0,.05);--tw-shadow-colored:0 1px 2px 0 var(--tw-shadow-color)}.ring-1{--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);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000)}.ring-inset{--tw-ring-inset:inset}.ring-black{--tw-ring-opacity:1;--tw-ring-color:rgb(0 0 0/var(--tw-ring-opacity))}.ring-gray-300{--tw-ring-opacity:1;--tw-ring-color:rgb(209 213 219/var(--tw-ring-opacity))}.ring-opacity-5{--tw-ring-opacity:0.05}.placeholder\:text-gray-400::-moz-placeholder{--tw-text-opacity:1;color:rgb(156 163 175/var(--tw-text-opacity))}.placeholder\:text-gray-400::placeholder{--tw-text-opacity:1;color:rgb(156 163 175/var(--tw-text-opacity))}.focus-within\:ring-2:focus-within{--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,0 0 #0000)}.focus-within\:ring-indigo-600:focus-within{--tw-ring-opacity:1;--tw-ring-color:rgb(79 70 229/var(--tw-ring-opacity))}.hover\:border-gray-200:hover{--tw-border-opacity:1;border-color:rgb(229 231 235/var(--tw-border-opacity))}.hover\:text-gray-500:hover{--tw-text-opacity:1;color:rgb(107 114 128/var(--tw-text-opacity))}.hover\:text-gray-700:hover{--tw-text-opacity:1;color:rgb(55 65 81/var(--tw-text-opacity))}.hover\:text-indigo-900:hover{--tw-text-opacity:1;color:rgb(49 46 129/var(--tw-text-opacity))}.focus\:outline-none:focus{outline:2px solid transparent;outline-offset:2px}.focus\:ring-0: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(var(--tw-ring-offset-width)) var(--tw-ring-color)}.focus\:ring-0:focus,.focus\:ring-2: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-indigo-500:focus{--tw-ring-opacity:1;--tw-ring-color:rgb(99 102 241/var(--tw-ring-opacity))}.focus\:ring-offset-2:focus{--tw-ring-offset-width:2px}.group:hover .group-hover\:flex{display:flex}.group.active .group-\[\.active\]\:bg-indigo-100{--tw-bg-opacity:1;background-color:rgb(224 231 255/var(--tw-bg-opacity))}.group.active .group-\[\.active\]\:text-indigo-600{--tw-text-opacity:1;color:rgb(79 70 229/var(--tw-text-opacity))}@media (min-width:640px){.sm\:ml-16{margin-left:4rem}.sm\:mt-0{margin-top:0}.sm\:flex{display:flex}.sm\:flex-auto{flex:1 1 auto}.sm\:flex-none{flex:none}.sm\:items-center{align-items:center}.sm\:rounded-lg{border-radius:.5rem}.sm\:px-6{padding-right:1.5rem}.sm\:pl-6,.sm\:px-6{padding-left:1.5rem}.sm\:pr-6{padding-right:1.5rem}.sm\:text-sm{font-size:.875rem;line-height:1.25rem}.sm\:leading-6{line-height:1.5rem}}@media (min-width:768px){.md\:inline-block{display:inline-block}}@media (min-width:1024px){.lg\:px-8{padding-left:2rem;padding-right:2rem}}
|
@@ -3,14 +3,13 @@ module Rosetta
|
|
3
3
|
extend ActiveSupport::Concern
|
4
4
|
|
5
5
|
included do
|
6
|
-
|
6
|
+
before_action :set_locale
|
7
7
|
end
|
8
8
|
|
9
9
|
private
|
10
10
|
|
11
11
|
def set_locale(&action)
|
12
12
|
@locale = Locale.find(params[:locale_id])
|
13
|
-
Rosetta.with_locale(@locale, &action)
|
14
13
|
end
|
15
14
|
end
|
16
15
|
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module Rosetta
|
2
|
+
class DefaultLocalesController < ApplicationController
|
3
|
+
before_action :ensure_configuration_needed
|
4
|
+
|
5
|
+
def new
|
6
|
+
@locale = Locale.new
|
7
|
+
end
|
8
|
+
|
9
|
+
def create
|
10
|
+
@locale = Locale.build_as_default(locale_params)
|
11
|
+
|
12
|
+
if @locale.save
|
13
|
+
redirect_to locales_path
|
14
|
+
else
|
15
|
+
render :new, status: :unprocessable_entity
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
def ensure_configuration_needed
|
22
|
+
redirect_to root_path if Locale.default_locale
|
23
|
+
end
|
24
|
+
|
25
|
+
def locale_params
|
26
|
+
params.require(:locale).permit(:name, :code)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -3,7 +3,7 @@ module Rosetta
|
|
3
3
|
include LocaleScoped
|
4
4
|
|
5
5
|
def index
|
6
|
-
@pagy, @
|
6
|
+
@pagy, @text_entries = pagy(scope)
|
7
7
|
|
8
8
|
render "rosetta/locales/translations/index"
|
9
9
|
end
|
@@ -11,7 +11,7 @@ module Rosetta
|
|
11
11
|
private
|
12
12
|
|
13
13
|
def scope
|
14
|
-
|
14
|
+
TextEntry.with_translated_version(@locale).where(locale: Locale.default_locale)
|
15
15
|
end
|
16
16
|
end
|
17
17
|
end
|
@@ -1,7 +1,9 @@
|
|
1
1
|
module Rosetta
|
2
2
|
class LocalesController < ApplicationController
|
3
|
+
before_action :ensure_default_locale_exists, only: :index
|
4
|
+
|
3
5
|
def index
|
4
|
-
@locales =
|
6
|
+
@locales = Locale.order(default: :desc)
|
5
7
|
end
|
6
8
|
|
7
9
|
def new
|
@@ -24,6 +26,10 @@ module Rosetta
|
|
24
26
|
|
25
27
|
private
|
26
28
|
|
29
|
+
def ensure_default_locale_exists
|
30
|
+
redirect_to new_default_locale_path unless Locale.default_locale
|
31
|
+
end
|
32
|
+
|
27
33
|
def locale_params
|
28
34
|
params.require(:locale).permit(:name, :code)
|
29
35
|
end
|
@@ -2,34 +2,25 @@ module Rosetta
|
|
2
2
|
class TranslationsController < ApplicationController
|
3
3
|
include LocaleScoped
|
4
4
|
|
5
|
-
before_action :
|
6
|
-
before_action :set_translation
|
5
|
+
before_action :set_text_entry
|
7
6
|
|
8
7
|
def edit
|
9
8
|
end
|
10
9
|
|
11
10
|
def update
|
12
|
-
|
13
|
-
@translation_key.translation_in_current_locale = nil
|
14
|
-
else
|
15
|
-
@translation.update(translation_params)
|
16
|
-
end
|
11
|
+
@text_entry.update(text_entry_params)
|
17
12
|
|
18
|
-
render partial: "rosetta/
|
13
|
+
render partial: "rosetta/text_entries/text_entry_with_translation", locals: { text_entry: @text_entry }
|
19
14
|
end
|
20
15
|
|
21
16
|
private
|
22
17
|
|
23
|
-
def
|
24
|
-
@
|
18
|
+
def set_text_entry
|
19
|
+
@text_entry = TextEntry.find(params[:text_entry_id])
|
25
20
|
end
|
26
21
|
|
27
|
-
def
|
28
|
-
|
29
|
-
end
|
30
|
-
|
31
|
-
def translation_params
|
32
|
-
params.require(:translation).permit(:value)
|
22
|
+
def text_entry_params
|
23
|
+
params.require(:text_entry).permit(:"content_#{@locale.code}")
|
33
24
|
end
|
34
25
|
end
|
35
26
|
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
module Rosetta
|
2
|
+
class AutodiscoveryJob < Rosetta::ApplicationJob
|
3
|
+
queue_as { Rosetta.config.queues[:autodiscovery] }
|
4
|
+
|
5
|
+
discard_on ActiveRecord::RecordNotUnique
|
6
|
+
|
7
|
+
def perform(content)
|
8
|
+
TextEntry.create!(content: content, locale: Locale.default_locale)
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
@@ -1,31 +1,39 @@
|
|
1
1
|
module Rosetta
|
2
2
|
class Locale < ApplicationRecord
|
3
|
+
class_attribute :registered_classes_for_translations, default: []
|
4
|
+
|
3
5
|
CODE_FORMAT = /\A[a-zA-Z]+(-[a-zA-Z]+)?\z/
|
4
6
|
|
5
7
|
validates :name, :code, presence: true
|
6
8
|
validates :code, uniqueness: true
|
7
9
|
validates :code, format: { with: CODE_FORMAT, message: "must only contain letters separated by an optional dash" }
|
8
10
|
|
9
|
-
has_many :
|
11
|
+
has_many :text_entries, dependent: :destroy
|
12
|
+
after_create_commit :notify_translated_models
|
10
13
|
|
11
14
|
class << self
|
12
15
|
def available_locales
|
13
|
-
|
16
|
+
all
|
17
|
+
end
|
18
|
+
|
19
|
+
def build_as_default(params)
|
20
|
+
new(params.merge(default: true))
|
14
21
|
end
|
15
22
|
|
16
23
|
def default_locale
|
17
|
-
@default_locale ||=
|
24
|
+
@default_locale ||= find_by(default: true)
|
18
25
|
end
|
19
|
-
end
|
20
26
|
|
21
|
-
|
22
|
-
|
27
|
+
def default_locale=(locale)
|
28
|
+
@default_locale = locale
|
29
|
+
end
|
30
|
+
def register_class_for_translation(klass)
|
31
|
+
registered_classes_for_translations << klass
|
32
|
+
end
|
23
33
|
end
|
24
34
|
|
25
|
-
def
|
26
|
-
|
27
|
-
readonly!
|
28
|
-
self
|
35
|
+
def notify_translated_models
|
36
|
+
registered_classes_for_translations.each { |klass| klass.translated_in(self) }
|
29
37
|
end
|
30
38
|
end
|
31
39
|
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module Rosetta
|
2
|
+
class TextEntry < ApplicationRecord
|
3
|
+
include Translated
|
4
|
+
|
5
|
+
translate_in_all_locales
|
6
|
+
|
7
|
+
belongs_to :locale
|
8
|
+
|
9
|
+
def self.create_later(content)
|
10
|
+
AutodiscoveryJob.perform_later(content)
|
11
|
+
end
|
12
|
+
|
13
|
+
def purge
|
14
|
+
destroy
|
15
|
+
rescue ActiveRecord::InvalidForeignKey
|
16
|
+
end
|
17
|
+
|
18
|
+
def purge_later
|
19
|
+
PurgeJob.perform_later(self)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -1,6 +1,15 @@
|
|
1
1
|
module Rosetta
|
2
2
|
class Translation < ApplicationRecord
|
3
|
-
belongs_to :
|
4
|
-
belongs_to :
|
3
|
+
belongs_to :target_locale, class_name: "Rosetta::Locale"
|
4
|
+
belongs_to :from, class_name: "Rosetta::TextEntry"
|
5
|
+
belongs_to :to, class_name: "Rosetta::TextEntry"
|
6
|
+
|
7
|
+
after_destroy_commit :purge_orphaned_text_entries_later
|
8
|
+
|
9
|
+
private
|
10
|
+
|
11
|
+
def purge_orphaned_text_entries_later
|
12
|
+
to.purge_later
|
13
|
+
end
|
5
14
|
end
|
6
15
|
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
<h1 class="text-3xl font-bold">
|
2
|
+
Setup default locale
|
3
|
+
</h1>
|
4
|
+
|
5
|
+
<div class="rounded-md bg-blue-50 p-4 mt-8">
|
6
|
+
<div class="flex">
|
7
|
+
<div class="flex-shrink-0">
|
8
|
+
<svg class="h-5 w-5 text-blue-400" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
|
9
|
+
<path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a.75.75 0 000 1.5h.253a.25.25 0 01.244.304l-.459 2.066A1.75 1.75 0 0010.747 15H11a.75.75 0 000-1.5h-.253a.25.25 0 01-.244-.304l.459-2.066A1.75 1.75 0 009.253 9H9z" clip-rule="evenodd" />
|
10
|
+
</svg>
|
11
|
+
</div>
|
12
|
+
<div class="ml-3">
|
13
|
+
<h3 class="font-medium text-blue-800">Rosetta needs a default locale set up to work properly</h3>
|
14
|
+
|
15
|
+
<p class="text-sm text-blue-700 mt-2">
|
16
|
+
The default locale is the locale the codebase is written in. It is not supposed to change in the future, though it can be edited if needed later on.
|
17
|
+
</p>
|
18
|
+
</div>
|
19
|
+
</div>
|
20
|
+
</div>
|
21
|
+
|
22
|
+
<%= form_with model: @locale, url: default_locale_path, class: "grid gap-2 mt-8" do |f| %>
|
23
|
+
<% if @locale.errors[:code].any? %>
|
24
|
+
<div class="rounded-md bg-red-50 p-4">
|
25
|
+
<div class="flex">
|
26
|
+
<div class="flex-shrink-0">
|
27
|
+
<svg class="h-5 w-5 text-red-400" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
|
28
|
+
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.28 7.22a.75.75 0 00-1.06 1.06L8.94 10l-1.72 1.72a.75.75 0 101.06 1.06L10 11.06l1.72 1.72a.75.75 0 101.06-1.06L11.06 10l1.72-1.72a.75.75 0 00-1.06-1.06L10 8.94 8.28 7.22z" clip-rule="evenodd" />
|
29
|
+
</svg>
|
30
|
+
</div>
|
31
|
+
<div class="ml-3">
|
32
|
+
<div class="text-sm text-red-700">
|
33
|
+
Please check the format of your locale code, and make sure it does not already exist.
|
34
|
+
</div>
|
35
|
+
</div>
|
36
|
+
</div>
|
37
|
+
</div>
|
38
|
+
<% end %>
|
39
|
+
|
40
|
+
<div>
|
41
|
+
<%= f.label :name, class: "label" %>
|
42
|
+
<%= f.text_field :name, placeholder: "E.g. English", class: "input max-w-xs", required: true %>
|
43
|
+
</div>
|
44
|
+
|
45
|
+
<div>
|
46
|
+
<%= f.label :code, class: "label" %>
|
47
|
+
<%= f.text_field :code, placeholder: "E.g. en", class: "input max-w-28", required: true %>
|
48
|
+
<p class="mt-1 text-sm leading-6 text-gray-600">
|
49
|
+
Downcase letters followed by an optional region specifier in uppercase letters, separated by a dash. E.g. <span class="badge">fr</span> or <span class="badge">en-GB</span>.
|
50
|
+
</p>
|
51
|
+
</div>
|
52
|
+
|
53
|
+
<%= f.submit "Setup default locale", class: "btn btn-primary mt-2" %>
|
54
|
+
<% end %>
|
@@ -8,11 +8,11 @@
|
|
8
8
|
</span>
|
9
9
|
</td>
|
10
10
|
<td class="relative whitespace-nowrap py-4 pl-3 pr-4 text-right text-sm font-medium sm:pr-6">
|
11
|
-
<% if locale.
|
11
|
+
<% if locale.default? %>
|
12
12
|
<span class="pill">Default locale</span>
|
13
13
|
<% end %>
|
14
14
|
|
15
|
-
<% unless locale.
|
15
|
+
<% unless locale.default? %>
|
16
16
|
<%= link_to "Manage", locale_translations_path(locale), class: "text-indigo-600 hover:text-indigo-900" %>
|
17
17
|
<% end %>
|
18
18
|
</td>
|
@@ -6,7 +6,7 @@
|
|
6
6
|
<%= tab_link_to locale_translations_missing_index_path(@locale) do %>
|
7
7
|
Missing
|
8
8
|
<span class="ml-3 hidden md:inline-block rounded-full px-2.5 py-0.5 text-xs font-medium bg-gray-100 text-gray-900 group-[.active]:bg-indigo-100 group-[.active]:text-indigo-600">
|
9
|
-
<%= Rosetta::
|
9
|
+
<%= Rosetta::TextEntry.where(locale: Rosetta::Locale.default_locale).missing_translation(@locale).size %>
|
10
10
|
</span>
|
11
11
|
<% end %>
|
12
12
|
</nav>
|
@@ -33,15 +33,16 @@
|
|
33
33
|
<table class="min-w-full divide-y divide-gray-300">
|
34
34
|
<thead class="bg-gray-50">
|
35
35
|
<tr class="divide-x divide-gray-200">
|
36
|
-
<th scope="col" class="w-1/2 py-3.5 pl-4 pr-3 text-left text-sm font-semibold text-gray-900 sm:pl-6">
|
36
|
+
<th scope="col" class="w-1/2 py-3.5 pl-4 pr-3 text-left text-sm font-semibold text-gray-900 sm:pl-6">Text Entry</th>
|
37
37
|
<th scope="col" class="w-1/2 px-3 py-3.5 text-left text-sm font-semibold text-gray-900">Translation</th>
|
38
38
|
</tr>
|
39
39
|
</thead>
|
40
40
|
|
41
41
|
<tbody class="divide-y divide-gray-200 bg-white">
|
42
42
|
<%= render(
|
43
|
-
collection: @
|
44
|
-
|
43
|
+
collection: @text_entries,
|
44
|
+
as: :text_entry,
|
45
|
+
partial: "rosetta/text_entries/text_entry_with_translation") %>
|
45
46
|
</tbody>
|
46
47
|
</table>
|
47
48
|
</div>
|
@@ -1,13 +1,13 @@
|
|
1
1
|
<tr class="divide-x divide-gray-200">
|
2
2
|
<td class="py-4 pl-4 pr-3 text-sm text-gray-900 sm:pl-6">
|
3
|
-
<%=
|
3
|
+
<%= text_entry.content %>
|
4
4
|
</td>
|
5
5
|
<td class="px-3 py-4 text-sm text-gray-900 group relative">
|
6
|
-
<%= turbo_frame_tag dom_id(
|
7
|
-
<%=
|
6
|
+
<%= turbo_frame_tag dom_id(text_entry) do %>
|
7
|
+
<%= text_entry.content_in(@locale) %>
|
8
8
|
|
9
9
|
<div class="hidden group-hover:flex absolute w-full h-full top-0 left-0 bg-indigo-50 bg-opacity-50 pr-4 items-center justify-end">
|
10
|
-
<%= link_to "edit",
|
10
|
+
<%= link_to "edit", edit_text_entry_translation_path(text_entry, locale_id: @locale.id), class: "text-indigo-600 hover:text-indigo-900 font-medium" %>
|
11
11
|
</div>
|
12
12
|
<% end %>
|
13
13
|
</td>
|
@@ -1,10 +1,10 @@
|
|
1
|
-
<%= turbo_frame_tag dom_id(@
|
2
|
-
<%= form_with model: @
|
1
|
+
<%= turbo_frame_tag dom_id(@text_entry) do %>
|
2
|
+
<%= form_with model: @text_entry, url: text_entry_translation_path(@text_entry), method: :patch, class: "relative" do |f| %>
|
3
3
|
<%= hidden_field_tag :locale_id, @locale.id %>
|
4
4
|
|
5
5
|
<div class="overflow-hidden rounded-lg shadow-sm ring-1 ring-inset ring-gray-300 focus-within:ring-2 focus-within:ring-indigo-600">
|
6
|
-
<%= f.label :
|
7
|
-
<%= f.text_area :
|
6
|
+
<%= f.label :"content_#{@locale.code}", "Translation", class: "sr-only" %>
|
7
|
+
<%= f.text_area :"content_#{@locale.code}", row: 3, autofocus: true, placeholder: "Enter your translation", class: "block w-full resize-none border-0 bg-transparent py-1.5 text-gray-900 placeholder:text-gray-400 focus:ring-0 sm:text-sm sm:leading-6" %>
|
8
8
|
|
9
9
|
<div class="py-2" aria-hidden="true">
|
10
10
|
<div class="py-px">
|
data/config/routes.rb
CHANGED
@@ -1,6 +1,8 @@
|
|
1
1
|
Rosetta::Engine.routes.draw do
|
2
2
|
root to: "locales#index"
|
3
3
|
|
4
|
+
resource :default_locale, only: %i[new create]
|
5
|
+
|
4
6
|
resources :locales do
|
5
7
|
scope module: :locales do
|
6
8
|
resources :translations, only: :index
|
@@ -12,7 +14,7 @@ Rosetta::Engine.routes.draw do
|
|
12
14
|
end
|
13
15
|
end
|
14
16
|
|
15
|
-
resources :
|
17
|
+
resources :text_entries do
|
16
18
|
resource :translation, only: %i[edit update]
|
17
19
|
end
|
18
20
|
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
class CreateRosettaTextEntries < ActiveRecord::Migration[7.2]
|
2
|
+
def change
|
3
|
+
create_table :rosetta_text_entries do |t|
|
4
|
+
t.text :content, null: false
|
5
|
+
t.references :locale, null: false
|
6
|
+
|
7
|
+
t.timestamps
|
8
|
+
|
9
|
+
t.index :content
|
10
|
+
t.index [ :locale_id, :content ], unique: true
|
11
|
+
t.foreign_key :rosetta_locales, column: :locale_id
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
class UpdateRosettaTranslations < ActiveRecord::Migration[7.2]
|
2
|
+
def change
|
3
|
+
change_table :rosetta_translations do |t|
|
4
|
+
t.remove :value
|
5
|
+
|
6
|
+
t.references :target_locale, null: false
|
7
|
+
t.references :from, null: false
|
8
|
+
t.references :to, null: false
|
9
|
+
|
10
|
+
t.foreign_key :rosetta_locales, column: :target_locale_id
|
11
|
+
t.foreign_key :rosetta_text_entries, column: :from_id
|
12
|
+
t.foreign_key :rosetta_text_entries, column: :to_id
|
13
|
+
|
14
|
+
t.index [ :target_locale_id, :from_id, :to_id ], name: :rosetta_translations_uniqueness, unique: true
|
15
|
+
end
|
16
|
+
|
17
|
+
remove_reference :rosetta_translations, :translation_key
|
18
|
+
remove_reference :rosetta_translations, :locale
|
19
|
+
end
|
20
|
+
end
|
@@ -1,17 +1,11 @@
|
|
1
1
|
module Rosetta
|
2
2
|
class Configuration
|
3
|
-
attr_reader :default_locale
|
4
3
|
attr_accessor :parent_controller_class
|
5
|
-
|
6
|
-
DefaultLocale = Struct.new(:name, :code)
|
4
|
+
attr_accessor :queues
|
7
5
|
|
8
6
|
def initialize
|
9
|
-
set_default_locale(name: "English", code: "en")
|
10
7
|
@parent_controller_class = "ActionController::Base"
|
11
|
-
|
12
|
-
|
13
|
-
def set_default_locale(name:, code:)
|
14
|
-
@default_locale = DefaultLocale.new(name, code)
|
8
|
+
@queues = {}
|
15
9
|
end
|
16
10
|
end
|
17
11
|
end
|
data/lib/rosetta/store.rb
CHANGED
@@ -13,19 +13,16 @@ module Rosetta
|
|
13
13
|
def initialize(locale)
|
14
14
|
@locale = locale
|
15
15
|
@cache_expiration_timestamp = @locale.updated_at
|
16
|
-
@autodiscovery_queue = Queue.new
|
17
|
-
|
18
|
-
start_key_autodiscovery!
|
19
16
|
end
|
20
17
|
|
21
|
-
def lookup(
|
22
|
-
if translations.has_key?(
|
23
|
-
translations[
|
18
|
+
def lookup(content)
|
19
|
+
if translations.has_key?(content)
|
20
|
+
translations[content]
|
24
21
|
else
|
25
|
-
|
22
|
+
TextEntry.create_later(content)
|
26
23
|
# Set the key in the translations store to locate it
|
27
24
|
# once only.
|
28
|
-
translations[
|
25
|
+
translations[content] = nil
|
29
26
|
end
|
30
27
|
end
|
31
28
|
|
@@ -47,29 +44,14 @@ module Rosetta
|
|
47
44
|
private
|
48
45
|
|
49
46
|
def load_translations
|
50
|
-
loaded_translations =
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
end
|
47
|
+
loaded_translations = TextEntry
|
48
|
+
.with_translated_version(@locale)
|
49
|
+
.where(locale: Rosetta::Locale.default_locale)
|
50
|
+
.map do |text_entry|
|
51
|
+
[ text_entry.content, text_entry.public_send(:"#{@locale.code}_translated_version")&.content ]
|
52
|
+
end.to_h
|
57
53
|
|
58
54
|
Concurrent::Hash.new.merge(loaded_translations)
|
59
55
|
end
|
60
|
-
|
61
|
-
def start_key_autodiscovery!
|
62
|
-
Thread.new do
|
63
|
-
Thread.current.name = "Rosetta #{locale.code} store thread"
|
64
|
-
|
65
|
-
loop do
|
66
|
-
key_value = @autodiscovery_queue.pop
|
67
|
-
|
68
|
-
unless TranslationKey.exists?(value: key_value)
|
69
|
-
TranslationKey.create!(value: key_value)
|
70
|
-
end
|
71
|
-
end
|
72
|
-
end
|
73
|
-
end
|
74
56
|
end
|
75
57
|
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
module Rosetta
|
2
|
+
class Translated::Create
|
3
|
+
attr_reader :content
|
4
|
+
|
5
|
+
def initialize(record, locale, content)
|
6
|
+
@record = record
|
7
|
+
@locale = locale
|
8
|
+
@content = content
|
9
|
+
end
|
10
|
+
|
11
|
+
def save
|
12
|
+
@record.public_send(:"#{@locale.code}_translation=", translation)
|
13
|
+
end
|
14
|
+
|
15
|
+
def translated_version
|
16
|
+
@translated_version ||= find_or_build_translated_version
|
17
|
+
end
|
18
|
+
|
19
|
+
def translation
|
20
|
+
@record.public_send(:"build_#{@locale.code}_translation", to: translated_version)
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def find_or_build_translated_version
|
26
|
+
find_translated_version || build_translated_version
|
27
|
+
end
|
28
|
+
|
29
|
+
def find_translated_version
|
30
|
+
TextEntry.find_by(locale: @locale, content: @content)
|
31
|
+
end
|
32
|
+
|
33
|
+
def build_translated_version
|
34
|
+
TextEntry.new(locale: @locale, content: @content)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module Rosetta
|
2
|
+
class Translated::Delete
|
3
|
+
attr_reader :content
|
4
|
+
|
5
|
+
def initialize(record, locale)
|
6
|
+
@record = record
|
7
|
+
@locale = locale
|
8
|
+
@content = nil
|
9
|
+
end
|
10
|
+
|
11
|
+
def save
|
12
|
+
@record.public_send(:"#{@locale.code}_translation=", nil)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
module Rosetta
|
2
|
+
module Translated
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
included do
|
6
|
+
scope :missing_translation, ->(locale) { where.missing(:"#{locale.code}_translation") }
|
7
|
+
scope :with_translated_version, ->(locale) { includes(:"#{locale.code}_translated_version") }
|
8
|
+
end
|
9
|
+
|
10
|
+
class_methods do
|
11
|
+
def translate_in_all_locales
|
12
|
+
Locale.all.each do |locale|
|
13
|
+
translated_in(locale)
|
14
|
+
end
|
15
|
+
|
16
|
+
Locale.register_class_for_translation(self)
|
17
|
+
end
|
18
|
+
|
19
|
+
def translated_in(locale)
|
20
|
+
has_one :"#{locale.code}_translation", -> { where(target_locale: locale) }, class_name: "Rosetta::Translation", foreign_key: :from_id, dependent: :destroy
|
21
|
+
has_one :"#{locale.code}_translated_version", through: :"#{locale.code}_translation", source: :to
|
22
|
+
|
23
|
+
define_method("content_#{locale.code}") do
|
24
|
+
if translation_changes[locale.code]
|
25
|
+
translation_changes[locale.code].content
|
26
|
+
else
|
27
|
+
public_send(:"#{locale.code}_translated_version")&.content
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
define_method("content_#{locale.code}=") do |localized_content|
|
32
|
+
translation_changes[locale.code] = if localized_content.blank?
|
33
|
+
Rosetta::Translated::Delete.new(self, locale)
|
34
|
+
else
|
35
|
+
Rosetta::Translated::Create.new(self, locale, localized_content)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
after_save { translation_changes[locale.code]&.save }
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def content_in(locale)
|
44
|
+
public_send("content_#{locale.code}")
|
45
|
+
end
|
46
|
+
|
47
|
+
def translation_changes
|
48
|
+
@translation_changes ||= {}
|
49
|
+
end
|
50
|
+
|
51
|
+
def reload(*)
|
52
|
+
super.tap { @translation_changes = nil }
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
data/lib/rosetta/version.rb
CHANGED
data/lib/rosetta-rails.rb
CHANGED
@@ -5,6 +5,10 @@ require "rosetta/locale_session"
|
|
5
5
|
require "rosetta/store"
|
6
6
|
require "rosetta/configuration"
|
7
7
|
|
8
|
+
require "rosetta/translated"
|
9
|
+
require "rosetta/translated/create"
|
10
|
+
require "rosetta/translated/delete"
|
11
|
+
|
8
12
|
module Rosetta
|
9
13
|
module Base
|
10
14
|
def locale
|
@@ -30,9 +34,13 @@ module Rosetta
|
|
30
34
|
Thread.current[:rosetta_locale_session] ||= LocaleSession.new
|
31
35
|
end
|
32
36
|
|
33
|
-
def translate(
|
37
|
+
def translate(content, locale: Rosetta.locale)
|
34
38
|
store = Store.for_locale(locale)
|
35
|
-
store.lookup(
|
39
|
+
store.lookup(content)
|
40
|
+
end
|
41
|
+
|
42
|
+
def available_locales
|
43
|
+
Locale.available_locales
|
36
44
|
end
|
37
45
|
|
38
46
|
def config
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: rosetta-rails
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Vincent Rolea
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2024-
|
11
|
+
date: 2024-10-03 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rails
|
@@ -100,6 +100,7 @@ files:
|
|
100
100
|
- app/assets/stylesheets/rosetta/application.css
|
101
101
|
- app/controllers/concerns/rosetta/locale_scoped.rb
|
102
102
|
- app/controllers/rosetta/application_controller.rb
|
103
|
+
- app/controllers/rosetta/default_locales_controller.rb
|
103
104
|
- app/controllers/rosetta/locales/deploys_controller.rb
|
104
105
|
- app/controllers/rosetta/locales/translations/missing_controller.rb
|
105
106
|
- app/controllers/rosetta/locales/translations_controller.rb
|
@@ -110,31 +111,41 @@ files:
|
|
110
111
|
- app/helpers/rosetta/navigation_helper.rb
|
111
112
|
- app/helpers/rosetta/translation_helper.rb
|
112
113
|
- app/jobs/rosetta/application_job.rb
|
114
|
+
- app/jobs/rosetta/autodiscovery_job.rb
|
115
|
+
- app/jobs/rosetta/purge_job.rb
|
113
116
|
- app/mailers/rosetta/application_mailer.rb
|
114
117
|
- app/models/rosetta/application_record.rb
|
115
118
|
- app/models/rosetta/locale.rb
|
119
|
+
- app/models/rosetta/text_entry.rb
|
116
120
|
- app/models/rosetta/translation.rb
|
117
|
-
- app/models/rosetta/translation_key.rb
|
118
121
|
- app/views/layouts/rosetta/_dialog.html.erb
|
119
122
|
- app/views/layouts/rosetta/_flashes.html.erb
|
120
123
|
- app/views/layouts/rosetta/_navbar.html.erb
|
121
124
|
- app/views/layouts/rosetta/application.html.erb
|
125
|
+
- app/views/rosetta/default_locales/new.html.erb
|
122
126
|
- app/views/rosetta/locales/_form.html.erb
|
123
127
|
- app/views/rosetta/locales/_locale.html.erb
|
124
128
|
- app/views/rosetta/locales/index.html.erb
|
125
129
|
- app/views/rosetta/locales/new.html.erb
|
126
130
|
- app/views/rosetta/locales/translations/_navigation.html.erb
|
127
|
-
- app/views/rosetta/locales/translations/_translation_key.html.erb
|
128
131
|
- app/views/rosetta/locales/translations/index.html.erb
|
132
|
+
- app/views/rosetta/text_entries/_text_entry_with_translation.html.erb
|
129
133
|
- app/views/rosetta/translations/edit.html.erb
|
130
134
|
- config/initializers/pagy.rb
|
131
135
|
- config/routes.rb
|
132
136
|
- db/migrate/20240830123523_create_rosetta_tables.rb
|
137
|
+
- db/migrate/20240923100651_add_default_to_rosetta_locales.rb
|
138
|
+
- db/migrate/20240930135507_create_rosetta_text_entries.rb
|
139
|
+
- db/migrate/20240930135810_update_rosetta_translations.rb
|
140
|
+
- db/migrate/20241002152043_drop_translation_keys.rb
|
133
141
|
- lib/rosetta-rails.rb
|
134
142
|
- lib/rosetta/configuration.rb
|
135
143
|
- lib/rosetta/engine.rb
|
136
144
|
- lib/rosetta/locale_session.rb
|
137
145
|
- lib/rosetta/store.rb
|
146
|
+
- lib/rosetta/translated.rb
|
147
|
+
- lib/rosetta/translated/create.rb
|
148
|
+
- lib/rosetta/translated/delete.rb
|
138
149
|
- lib/rosetta/version.rb
|
139
150
|
- lib/tasks/rosetta_tasks.rake
|
140
151
|
homepage: https://github.com/virolea/rosetta
|
@@ -1,7 +0,0 @@
|
|
1
|
-
module Rosetta
|
2
|
-
class TranslationKey < ApplicationRecord
|
3
|
-
has_many :translations, dependent: :destroy
|
4
|
-
# Note: Learn about the design decisions behind this: https://github.com/virolea/rosetta/issues/3
|
5
|
-
has_one :translation_in_current_locale, -> { where(locale_id: Rosetta.locale.id) }, class_name: "Translation", dependent: :destroy
|
6
|
-
end
|
7
|
-
end
|