rosetta-rails 0.1.1
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/MIT-LICENSE +20 -0
- data/README.md +28 -0
- data/Rakefile +8 -0
- data/app/assets/builds/rosetta/application.css +1 -0
- data/app/assets/builds/rosetta/application.js +8290 -0
- data/app/assets/config/rosetta_manifest.js +2 -0
- data/app/assets/javascripts/rosetta/application.js +2 -0
- data/app/assets/javascripts/rosetta/controllers/application.js +9 -0
- data/app/assets/javascripts/rosetta/controllers/dialog_controller.js +40 -0
- data/app/assets/javascripts/rosetta/controllers/index.js +4 -0
- data/app/assets/stylesheets/rosetta/application.css +49 -0
- data/app/controllers/concerns/rosetta/locale_scoped.rb +16 -0
- data/app/controllers/rosetta/application_controller.rb +5 -0
- data/app/controllers/rosetta/locales/deploys_controller.rb +12 -0
- data/app/controllers/rosetta/locales/translations/missing_controller.rb +9 -0
- data/app/controllers/rosetta/locales/translations_controller.rb +17 -0
- data/app/controllers/rosetta/locales_controller.rb +31 -0
- data/app/controllers/rosetta/translations_controller.rb +35 -0
- data/app/helpers/rosetta/application_helper.rb +7 -0
- data/app/helpers/rosetta/dialog_helper.rb +11 -0
- data/app/helpers/rosetta/navigation_helper.rb +21 -0
- data/app/helpers/rosetta/translation_helper.rb +9 -0
- data/app/jobs/rosetta/application_job.rb +4 -0
- data/app/mailers/rosetta/application_mailer.rb +6 -0
- data/app/models/rosetta/application_record.rb +5 -0
- data/app/models/rosetta/locale.rb +31 -0
- data/app/models/rosetta/translation.rb +6 -0
- data/app/models/rosetta/translation_key.rb +7 -0
- data/app/views/layouts/rosetta/_dialog.html.erb +25 -0
- data/app/views/layouts/rosetta/_flashes.html.erb +7 -0
- data/app/views/layouts/rosetta/_navbar.html.erb +7 -0
- data/app/views/layouts/rosetta/application.html.erb +25 -0
- data/app/views/rosetta/locales/_form.html.erb +33 -0
- data/app/views/rosetta/locales/_locale.html.erb +19 -0
- data/app/views/rosetta/locales/index.html.erb +31 -0
- data/app/views/rosetta/locales/new.html.erb +4 -0
- data/app/views/rosetta/locales/translations/_navigation.html.erb +14 -0
- data/app/views/rosetta/locales/translations/_translation_key.html.erb +14 -0
- data/app/views/rosetta/locales/translations/index.html.erb +52 -0
- data/app/views/rosetta/translations/edit.html.erb +21 -0
- data/config/initializers/pagy.rb +220 -0
- data/config/routes.rb +18 -0
- data/db/migrate/20240830123523_create_rosetta_tables.rb +33 -0
- data/lib/rosetta/configuration.rb +17 -0
- data/lib/rosetta/engine.rb +12 -0
- data/lib/rosetta/locale_session.rb +16 -0
- data/lib/rosetta/store.rb +75 -0
- data/lib/rosetta/version.rb +3 -0
- data/lib/rosetta-rails.rb +48 -0
- data/lib/tasks/rosetta_tasks.rake +4 -0
- metadata +163 -0
@@ -0,0 +1,40 @@
|
|
1
|
+
import { Controller } from "@hotwired/stimulus";
|
2
|
+
|
3
|
+
export default class extends Controller {
|
4
|
+
static targets = ["dialog", "content", "title", "titleSource", "turboFrame"];
|
5
|
+
|
6
|
+
initialize() {
|
7
|
+
this.loadingIndicator = this.turboFrameTarget.innerHTML;
|
8
|
+
}
|
9
|
+
|
10
|
+
open(e) {
|
11
|
+
e.preventDefault();
|
12
|
+
const link = e.currentTarget;
|
13
|
+
this.dialogTarget.showModal();
|
14
|
+
this.turboFrameTarget.src = link.href;
|
15
|
+
}
|
16
|
+
|
17
|
+
safeClose(e) {
|
18
|
+
if (this.contentTarget.contains(e.target)) return;
|
19
|
+
this.close();
|
20
|
+
}
|
21
|
+
|
22
|
+
close(e) {
|
23
|
+
this.dialogTarget.close();
|
24
|
+
this.resetContent();
|
25
|
+
}
|
26
|
+
|
27
|
+
titleSourceTargetConnected(e) {
|
28
|
+
const title = e.textContent;
|
29
|
+
this.setTitle(title);
|
30
|
+
}
|
31
|
+
|
32
|
+
resetContent() {
|
33
|
+
this.turboFrameTarget.innerHTML = this.loadingIndicator;
|
34
|
+
this.setTitle("");
|
35
|
+
}
|
36
|
+
|
37
|
+
setTitle(value) {
|
38
|
+
this.titleTarget.textContent = value;
|
39
|
+
}
|
40
|
+
}
|
@@ -0,0 +1,49 @@
|
|
1
|
+
@tailwind base;
|
2
|
+
@tailwind components;
|
3
|
+
@tailwind utilities;
|
4
|
+
|
5
|
+
@layer components {
|
6
|
+
.btn {
|
7
|
+
@apply rounded-md px-3 py-2 text-sm font-semibold shadow-sm focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2;
|
8
|
+
}
|
9
|
+
.btn-primary {
|
10
|
+
@apply bg-indigo-600 text-white hover:bg-indigo-500 focus-visible:outline-indigo-600;
|
11
|
+
}
|
12
|
+
.btn-secondary {
|
13
|
+
@apply bg-white text-gray-900 ring-1 ring-inset ring-gray-300 hover:bg-gray-50;
|
14
|
+
}
|
15
|
+
.label {
|
16
|
+
@apply text-sm font-medium leading-6 text-gray-900;
|
17
|
+
}
|
18
|
+
.input {
|
19
|
+
@apply block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6;
|
20
|
+
}
|
21
|
+
.badge {
|
22
|
+
@apply inline-flex items-center rounded-md bg-gray-50 px-2 py-1 text-xs font-medium text-gray-600 ring-1 ring-inset ring-gray-500/10;
|
23
|
+
}
|
24
|
+
.pill {
|
25
|
+
@apply inline-flex items-center rounded-full bg-green-50 px-2 py-1 text-xs font-medium text-green-700 ring-1 ring-inset ring-green-600/20;
|
26
|
+
}
|
27
|
+
.pagy {
|
28
|
+
@apply flex space-x-1 font-semibold text-sm text-gray-500 justify-center;
|
29
|
+
a:not(.gap) {
|
30
|
+
@apply block rounded-lg px-3 py-1 bg-gray-200;
|
31
|
+
&:hover {
|
32
|
+
@apply bg-gray-300;
|
33
|
+
}
|
34
|
+
&:not([href]) {
|
35
|
+
/* disabled links */
|
36
|
+
@apply text-gray-300 bg-gray-100 cursor-default;
|
37
|
+
}
|
38
|
+
&.current {
|
39
|
+
@apply text-white bg-indigo-600;
|
40
|
+
}
|
41
|
+
}
|
42
|
+
label {
|
43
|
+
@apply inline-block whitespace-nowrap bg-gray-200 rounded-lg px-3 py-0.5;
|
44
|
+
input {
|
45
|
+
@apply bg-gray-100 border-none rounded-md;
|
46
|
+
}
|
47
|
+
}
|
48
|
+
}
|
49
|
+
}
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module Rosetta
|
2
|
+
module LocaleScoped
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
included do
|
6
|
+
around_action :set_locale
|
7
|
+
end
|
8
|
+
|
9
|
+
private
|
10
|
+
|
11
|
+
def set_locale(&action)
|
12
|
+
@locale = Locale.find(params[:locale_id])
|
13
|
+
Rosetta.with_locale(@locale, &action)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
module Rosetta
|
2
|
+
class Locales::DeploysController < ApplicationController
|
3
|
+
include LocaleScoped
|
4
|
+
|
5
|
+
def create
|
6
|
+
@locale.touch
|
7
|
+
flash[:notice] = "#{@locale.name} changes have been deployed."
|
8
|
+
|
9
|
+
redirect_back(fallback_location: locale_translations_path(@locale))
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module Rosetta
|
2
|
+
class Locales::TranslationsController < ApplicationController
|
3
|
+
include LocaleScoped
|
4
|
+
|
5
|
+
def index
|
6
|
+
@pagy, @translation_keys = pagy(scope)
|
7
|
+
|
8
|
+
render "rosetta/locales/translations/index"
|
9
|
+
end
|
10
|
+
|
11
|
+
private
|
12
|
+
|
13
|
+
def scope
|
14
|
+
TranslationKey.includes(:translation_in_current_locale)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module Rosetta
|
2
|
+
class LocalesController < ApplicationController
|
3
|
+
def index
|
4
|
+
@locales = [ Locale.default_locale ] + Locale.all
|
5
|
+
end
|
6
|
+
|
7
|
+
def new
|
8
|
+
@locale = Locale.new
|
9
|
+
end
|
10
|
+
|
11
|
+
def create
|
12
|
+
@locale = Locale.new(locale_params)
|
13
|
+
|
14
|
+
if @locale.save
|
15
|
+
redirect_to locales_path
|
16
|
+
else
|
17
|
+
render turbo_stream: turbo_stream.update(
|
18
|
+
:dialog_content,
|
19
|
+
partial: "form",
|
20
|
+
locals: { locale: @locale }
|
21
|
+
)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def locale_params
|
28
|
+
params.require(:locale).permit(:name, :code)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
module Rosetta
|
2
|
+
class TranslationsController < ApplicationController
|
3
|
+
include LocaleScoped
|
4
|
+
|
5
|
+
before_action :set_translation_key
|
6
|
+
before_action :set_translation
|
7
|
+
|
8
|
+
def edit
|
9
|
+
end
|
10
|
+
|
11
|
+
def update
|
12
|
+
if translation_params[:value].blank?
|
13
|
+
@translation_key.translation_in_current_locale = nil
|
14
|
+
else
|
15
|
+
@translation.update(translation_params)
|
16
|
+
end
|
17
|
+
|
18
|
+
render partial: "rosetta/locales/translations/translation_key", locals: { translation_key: @translation_key }
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def set_translation_key
|
24
|
+
@translation_key = TranslationKey.find(params[:translation_key_id])
|
25
|
+
end
|
26
|
+
|
27
|
+
def set_translation
|
28
|
+
@translation = @translation_key.translation_in_current_locale || @translation_key.build_translation_in_current_locale
|
29
|
+
end
|
30
|
+
|
31
|
+
def translation_params
|
32
|
+
params.require(:translation).permit(:value)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module Rosetta
|
2
|
+
module NavigationHelper
|
3
|
+
def tab_link_to(name = nil, options = nil, html_options = nil, &block)
|
4
|
+
is_current_page = current_page?(block_given? ? name : options)
|
5
|
+
|
6
|
+
css_classes = class_names(
|
7
|
+
"flex group whitespace-nowrap border-b-2 px-1 py-4 text-sm font-medium",
|
8
|
+
"active border-indigo-500 text-indigo-600": is_current_page,
|
9
|
+
"border-transparent text-gray-500 hover:border-gray-200 hover:text-gray-700": !is_current_page
|
10
|
+
)
|
11
|
+
|
12
|
+
html_options = { class: css_classes, aria: { current: "page" } }
|
13
|
+
|
14
|
+
if block_given?
|
15
|
+
link_to(name, html_options, &block)
|
16
|
+
else
|
17
|
+
link_to(name, options, html_options)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module Rosetta
|
2
|
+
class Locale < ApplicationRecord
|
3
|
+
CODE_FORMAT = /\A[a-zA-Z]+(-[a-zA-Z]+)?\z/
|
4
|
+
|
5
|
+
validates :name, :code, presence: true
|
6
|
+
validates :code, uniqueness: true
|
7
|
+
validates :code, format: { with: CODE_FORMAT, message: "must only contain letters separated by an optional dash" }
|
8
|
+
|
9
|
+
has_many :translations, dependent: :destroy
|
10
|
+
|
11
|
+
class << self
|
12
|
+
def available_locales
|
13
|
+
[ Locale.default_locale ] + all
|
14
|
+
end
|
15
|
+
|
16
|
+
def default_locale
|
17
|
+
@default_locale ||= new(Rosetta.config.default_locale.to_h).as_default
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def default_locale?
|
22
|
+
@default
|
23
|
+
end
|
24
|
+
|
25
|
+
def as_default
|
26
|
+
@default = true
|
27
|
+
readonly!
|
28
|
+
self
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,7 @@
|
|
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
|
@@ -0,0 +1,25 @@
|
|
1
|
+
<dialog class="absolute max-w-4xl w-full mx-auto p-12 top-4 left-0 m-0 h-full bg-transparent" data-dialog-target="dialog" data-action="close->dialog#resetContent click->dialog#safeClose">
|
2
|
+
<div class="bg-white shadow ring-1 ring-black ring-opacity-5 max-w-xl mt-4 mx-auto rounded-lg overflow-hidden w-full" data-dialog-target="content">
|
3
|
+
<div class="flex justify-between bg-gray-50 py-4 px-6 text-gray-900 font-semibold border-b border-gray-300">
|
4
|
+
<h4 data-dialog-target="title"></h4>
|
5
|
+
|
6
|
+
<button type="button" class="rounded-md text-gray-400 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2" data-action="dialog#close">
|
7
|
+
<span class="sr-only">Close</span>
|
8
|
+
<svg class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" aria-hidden="true">
|
9
|
+
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
|
10
|
+
</svg>
|
11
|
+
</button>
|
12
|
+
</div>
|
13
|
+
|
14
|
+
<div class="p-4">
|
15
|
+
<%= turbo_frame_tag "dialog_content", data: { dialog_target: "turboFrame" } do %>
|
16
|
+
<div class="flex justify-around py-12">
|
17
|
+
<svg class="animate-spin h-5 w-5 text-black" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
18
|
+
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
19
|
+
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
20
|
+
</svg>
|
21
|
+
</div>
|
22
|
+
<% end %>
|
23
|
+
</div>
|
24
|
+
</div>
|
25
|
+
</dialog>
|
@@ -0,0 +1,25 @@
|
|
1
|
+
<!DOCTYPE html>
|
2
|
+
<html class="h-full">
|
3
|
+
<head>
|
4
|
+
<title>Rosetta</title>
|
5
|
+
<%= csrf_meta_tags %>
|
6
|
+
<%= csp_meta_tag %>
|
7
|
+
|
8
|
+
<%= yield :head %>
|
9
|
+
|
10
|
+
<%= stylesheet_link_tag "rosetta/application", media: "all" %>
|
11
|
+
<%= javascript_include_tag "rosetta/application", defer: true %>
|
12
|
+
</head>
|
13
|
+
<body class="h-full" data-controller="dialog">
|
14
|
+
<div class="min-h-full">
|
15
|
+
<%= render "layouts/rosetta/navbar" %>
|
16
|
+
<%= render "layouts/rosetta/flashes" %>
|
17
|
+
|
18
|
+
<main class="mx-auto max-w-4xl px-4 sm:px-6 lg:px-8 py-10">
|
19
|
+
<%= yield %>
|
20
|
+
</main>
|
21
|
+
</div>
|
22
|
+
|
23
|
+
<%= render "layouts/rosetta/dialog" %>
|
24
|
+
</body>
|
25
|
+
</html>
|
@@ -0,0 +1,33 @@
|
|
1
|
+
<%= form_with model: locale, class: "grid gap-2", data: { turbo_frame: "_top" } do |f| %>
|
2
|
+
<% if @locale.errors[:code].any? %>
|
3
|
+
<div class="rounded-md bg-red-50 p-4">
|
4
|
+
<div class="flex">
|
5
|
+
<div class="flex-shrink-0">
|
6
|
+
<svg class="h-5 w-5 text-red-400" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
|
7
|
+
<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" />
|
8
|
+
</svg>
|
9
|
+
</div>
|
10
|
+
<div class="ml-3">
|
11
|
+
<div class="text-sm text-red-700">
|
12
|
+
Please check the format of your locale code, and make sure it does not already exist.
|
13
|
+
</div>
|
14
|
+
</div>
|
15
|
+
</div>
|
16
|
+
</div>
|
17
|
+
<% end %>
|
18
|
+
|
19
|
+
<div>
|
20
|
+
<%= f.label :name, class: "label" %>
|
21
|
+
<%= f.text_field :name, placeholder: "E.g. French", class: "input max-w-xs", required: true %>
|
22
|
+
</div>
|
23
|
+
|
24
|
+
<div>
|
25
|
+
<%= f.label :code, class: "label" %>
|
26
|
+
<%= f.text_field :code, placeholder: "E.g. fr, en-GB", class: "input max-w-28", required: true %>
|
27
|
+
<p class="mt-1 text-sm leading-6 text-gray-600">
|
28
|
+
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>.
|
29
|
+
</p>
|
30
|
+
</div>
|
31
|
+
|
32
|
+
<%= f.submit class: "btn btn-primary mt-2" %>
|
33
|
+
<% end %>
|
@@ -0,0 +1,19 @@
|
|
1
|
+
<tr>
|
2
|
+
<td class="whitespace-nowrap py-4 pl-4 pr-3 text-sm font-medium text-gray-900 sm:pl-6">
|
3
|
+
<%= locale.name %>
|
4
|
+
</td>
|
5
|
+
<td class="whitespace-nowrap px-3 py-4 text-sm text-gray-500">
|
6
|
+
<span class="badge">
|
7
|
+
<%= locale.code %>
|
8
|
+
</span>
|
9
|
+
</td>
|
10
|
+
<td class="relative whitespace-nowrap py-4 pl-3 pr-4 text-right text-sm font-medium sm:pr-6">
|
11
|
+
<% if locale.default_locale? %>
|
12
|
+
<span class="pill">Default locale</span>
|
13
|
+
<% end %>
|
14
|
+
|
15
|
+
<% unless locale.default_locale? %>
|
16
|
+
<%= link_to "Manage", locale_translations_path(locale), class: "text-indigo-600 hover:text-indigo-900" %>
|
17
|
+
<% end %>
|
18
|
+
</td>
|
19
|
+
</tr>
|
@@ -0,0 +1,31 @@
|
|
1
|
+
<div class="sm:flex sm:items-center">
|
2
|
+
<div class="sm:flex-auto">
|
3
|
+
<h1 class="text-base font-semibold leading-6 text-gray-900">Locales</h1>
|
4
|
+
<p class="mt-2 text-sm text-gray-700">
|
5
|
+
Manage the locales for your project.
|
6
|
+
</p>
|
7
|
+
</div>
|
8
|
+
|
9
|
+
<div class="mt-4 sm:ml-16 sm:mt-0 sm:flex-none">
|
10
|
+
<%= link_to "Add locale", new_locale_path, class: "btn btn-primary", data: { action: "dialog#open" } %>
|
11
|
+
</div>
|
12
|
+
</div>
|
13
|
+
|
14
|
+
<div class="mt-8">
|
15
|
+
<div class="overflow-hidden shadow ring-1 ring-black ring-opacity-5 sm:rounded-lg">
|
16
|
+
<table class="min-w-full divide-y divide-gray-300">
|
17
|
+
<thead class="bg-gray-50">
|
18
|
+
<tr>
|
19
|
+
<th scope="col" class="py-3.5 pl-4 pr-3 text-left text-sm font-semibold text-gray-900 sm:pl-6">Name</th>
|
20
|
+
<th scope="col" class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900">Key</th>
|
21
|
+
<th scope="col" class="relative py-3.5 pl-3 pr-4 sm:pr-6">
|
22
|
+
<span class="sr-only">Manage</span>
|
23
|
+
</th>
|
24
|
+
</tr>
|
25
|
+
</thead>
|
26
|
+
<tbody class="divide-y divide-gray-200 bg-white">
|
27
|
+
<%= render @locales %>
|
28
|
+
</tbody>
|
29
|
+
</table>
|
30
|
+
</div>
|
31
|
+
</div>
|
@@ -0,0 +1,14 @@
|
|
1
|
+
<div class="mt-6">
|
2
|
+
<div class="border-b border-gray-200 flex justify-between items-center">
|
3
|
+
<nav class="-mb-px flex space-x-8" aria-label="Tabs">
|
4
|
+
<%= tab_link_to "All", locale_translations_path(@locale) %>
|
5
|
+
|
6
|
+
<%= tab_link_to locale_translations_missing_index_path(@locale) do %>
|
7
|
+
Missing
|
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::TranslationKey.where.missing(:translation_in_current_locale).size %>
|
10
|
+
</span>
|
11
|
+
<% end %>
|
12
|
+
</nav>
|
13
|
+
</div>
|
14
|
+
</div>
|
@@ -0,0 +1,14 @@
|
|
1
|
+
<tr class="divide-x divide-gray-200">
|
2
|
+
<td class="py-4 pl-4 pr-3 text-sm text-gray-900 sm:pl-6">
|
3
|
+
<%= translation_key.value %>
|
4
|
+
</td>
|
5
|
+
<td class="px-3 py-4 text-sm text-gray-900 group relative">
|
6
|
+
<%= turbo_frame_tag dom_id(translation_key) do %>
|
7
|
+
<%= translation_key.translation_in_current_locale&.value %>
|
8
|
+
|
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", edit_translation_key_translation_path(translation_key, locale_id: @locale.id), class: "text-indigo-600 hover:text-indigo-900 font-medium" %>
|
11
|
+
</div>
|
12
|
+
<% end %>
|
13
|
+
</td>
|
14
|
+
</tr>
|
@@ -0,0 +1,52 @@
|
|
1
|
+
<div class="sm:flex sm:items-center">
|
2
|
+
<div class="sm:flex-auto">
|
3
|
+
<nav class="flex justify-between items-center">
|
4
|
+
<ol role="list" class="flex items-center space-x-2">
|
5
|
+
<li>
|
6
|
+
<%= link_to "Locales", locales_path, class: "text-base font-semibold leading-6 text-indigo-600 hover:text-indigo-900" %>
|
7
|
+
</li>
|
8
|
+
|
9
|
+
<li>
|
10
|
+
<div class="flex items-center">
|
11
|
+
<svg class="h-5 w-5 flex-shrink-0 text-gray-900" fill="currentColor" viewBox="0 0 20 20" aria-hidden="true">
|
12
|
+
<path d="M5.555 17.776l8-16 .894.448-8 16-.894-.448z" />
|
13
|
+
</svg>
|
14
|
+
|
15
|
+
<div class="ml-2 text-base font-semibold leading-6 text-gray-900"><%= @locale.name %></div>
|
16
|
+
</div>
|
17
|
+
</li>
|
18
|
+
</ol>
|
19
|
+
|
20
|
+
<%= button_to "Deploy locale", locale_deploys_path(@locale), class: "btn btn-primary" %>
|
21
|
+
</nav>
|
22
|
+
|
23
|
+
<p class="mt-2 text-sm text-gray-700">
|
24
|
+
Manage translations for this locale.
|
25
|
+
</p>
|
26
|
+
</div>
|
27
|
+
</div>
|
28
|
+
|
29
|
+
<%= render "rosetta/locales/translations/navigation" %>
|
30
|
+
|
31
|
+
<div class="mt-4">
|
32
|
+
<div class="overflow-hidden shadow ring-1 ring-black ring-opacity-5 sm:rounded-lg">
|
33
|
+
<table class="min-w-full divide-y divide-gray-300">
|
34
|
+
<thead class="bg-gray-50">
|
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">Translation Key</th>
|
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
|
+
</tr>
|
39
|
+
</thead>
|
40
|
+
|
41
|
+
<tbody class="divide-y divide-gray-200 bg-white">
|
42
|
+
<%= render(
|
43
|
+
collection: @translation_keys,
|
44
|
+
partial: "rosetta/locales/translations/translation_key") %>
|
45
|
+
</tbody>
|
46
|
+
</table>
|
47
|
+
</div>
|
48
|
+
</div>
|
49
|
+
|
50
|
+
<div class="mt-4">
|
51
|
+
<%== pagy_nav(@pagy) %>
|
52
|
+
</div>
|
@@ -0,0 +1,21 @@
|
|
1
|
+
<%= turbo_frame_tag dom_id(@translation_key) do %>
|
2
|
+
<%= form_with model: @translation, url: translation_key_translation_path(@translation_key), method: :patch, class: "relative" do |f| %>
|
3
|
+
<%= hidden_field_tag :locale_id, @locale.id %>
|
4
|
+
|
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 :value, "Translation", class: "sr-only" %>
|
7
|
+
<%= f.text_area :value, 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
|
+
|
9
|
+
<div class="py-2" aria-hidden="true">
|
10
|
+
<div class="py-px">
|
11
|
+
<div class="h-9"></div>
|
12
|
+
</div>
|
13
|
+
</div>
|
14
|
+
</div>
|
15
|
+
|
16
|
+
<div class="absolute inset-x-0 bottom-0 flex justify-end gap-2 py-2 pl-3 pr-2">
|
17
|
+
<%= link_to "Discard", locale_translations_path(@locale), class: "btn btn-secondary" %>
|
18
|
+
<%= f.submit "Save", class: "inline btn btn-primary" %>
|
19
|
+
</div>
|
20
|
+
<% end %>
|
21
|
+
<% end %>
|