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.
Files changed (52) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +28 -0
  4. data/Rakefile +8 -0
  5. data/app/assets/builds/rosetta/application.css +1 -0
  6. data/app/assets/builds/rosetta/application.js +8290 -0
  7. data/app/assets/config/rosetta_manifest.js +2 -0
  8. data/app/assets/javascripts/rosetta/application.js +2 -0
  9. data/app/assets/javascripts/rosetta/controllers/application.js +9 -0
  10. data/app/assets/javascripts/rosetta/controllers/dialog_controller.js +40 -0
  11. data/app/assets/javascripts/rosetta/controllers/index.js +4 -0
  12. data/app/assets/stylesheets/rosetta/application.css +49 -0
  13. data/app/controllers/concerns/rosetta/locale_scoped.rb +16 -0
  14. data/app/controllers/rosetta/application_controller.rb +5 -0
  15. data/app/controllers/rosetta/locales/deploys_controller.rb +12 -0
  16. data/app/controllers/rosetta/locales/translations/missing_controller.rb +9 -0
  17. data/app/controllers/rosetta/locales/translations_controller.rb +17 -0
  18. data/app/controllers/rosetta/locales_controller.rb +31 -0
  19. data/app/controllers/rosetta/translations_controller.rb +35 -0
  20. data/app/helpers/rosetta/application_helper.rb +7 -0
  21. data/app/helpers/rosetta/dialog_helper.rb +11 -0
  22. data/app/helpers/rosetta/navigation_helper.rb +21 -0
  23. data/app/helpers/rosetta/translation_helper.rb +9 -0
  24. data/app/jobs/rosetta/application_job.rb +4 -0
  25. data/app/mailers/rosetta/application_mailer.rb +6 -0
  26. data/app/models/rosetta/application_record.rb +5 -0
  27. data/app/models/rosetta/locale.rb +31 -0
  28. data/app/models/rosetta/translation.rb +6 -0
  29. data/app/models/rosetta/translation_key.rb +7 -0
  30. data/app/views/layouts/rosetta/_dialog.html.erb +25 -0
  31. data/app/views/layouts/rosetta/_flashes.html.erb +7 -0
  32. data/app/views/layouts/rosetta/_navbar.html.erb +7 -0
  33. data/app/views/layouts/rosetta/application.html.erb +25 -0
  34. data/app/views/rosetta/locales/_form.html.erb +33 -0
  35. data/app/views/rosetta/locales/_locale.html.erb +19 -0
  36. data/app/views/rosetta/locales/index.html.erb +31 -0
  37. data/app/views/rosetta/locales/new.html.erb +4 -0
  38. data/app/views/rosetta/locales/translations/_navigation.html.erb +14 -0
  39. data/app/views/rosetta/locales/translations/_translation_key.html.erb +14 -0
  40. data/app/views/rosetta/locales/translations/index.html.erb +52 -0
  41. data/app/views/rosetta/translations/edit.html.erb +21 -0
  42. data/config/initializers/pagy.rb +220 -0
  43. data/config/routes.rb +18 -0
  44. data/db/migrate/20240830123523_create_rosetta_tables.rb +33 -0
  45. data/lib/rosetta/configuration.rb +17 -0
  46. data/lib/rosetta/engine.rb +12 -0
  47. data/lib/rosetta/locale_session.rb +16 -0
  48. data/lib/rosetta/store.rb +75 -0
  49. data/lib/rosetta/version.rb +3 -0
  50. data/lib/rosetta-rails.rb +48 -0
  51. data/lib/tasks/rosetta_tasks.rake +4 -0
  52. metadata +163 -0
@@ -0,0 +1,2 @@
1
+ //= link_directory ../builds/rosetta .css
2
+ //= link_directory ../builds/rosetta .js
@@ -0,0 +1,2 @@
1
+ import "@hotwired/turbo-rails";
2
+ import "./controllers";
@@ -0,0 +1,9 @@
1
+ import { Application } from "@hotwired/stimulus";
2
+
3
+ const application = Application.start();
4
+
5
+ // Configure Stimulus development experience
6
+ application.debug = false;
7
+ window.Stimulus = application;
8
+
9
+ export { application };
@@ -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,4 @@
1
+ import { application } from "./application";
2
+
3
+ import DialogController from "./dialog_controller";
4
+ application.register("dialog", DialogController);
@@ -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,5 @@
1
+ module Rosetta
2
+ class ApplicationController < Rosetta.config.parent_controller_class.constantize
3
+ include Pagy::Backend
4
+ end
5
+ 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,9 @@
1
+ module Rosetta
2
+ class Locales::Translations::MissingController < Locales::TranslationsController
3
+ private
4
+
5
+ def scope
6
+ super.where.missing(:translation_in_current_locale)
7
+ end
8
+ end
9
+ 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,7 @@
1
+ module Rosetta
2
+ module ApplicationHelper
3
+ include Pagy::Frontend
4
+ include DialogHelper
5
+ include NavigationHelper
6
+ end
7
+ end
@@ -0,0 +1,11 @@
1
+ module Rosetta
2
+ module DialogHelper
3
+ def within_modal(&block)
4
+ turbo_frame_tag :dialog_content, &block
5
+ end
6
+
7
+ def modal_title(title)
8
+ tag.h1(title, class: "hidden", data: { dialog_target: "titleSource" })
9
+ end
10
+ end
11
+ 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,9 @@
1
+ module Rosetta
2
+ module TranslationHelper
3
+ def _(key, locale: Rosetta.locale)
4
+ return key if Rosetta.locale.default_locale?
5
+
6
+ Rosetta.translate(key, locale: locale) || key
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,4 @@
1
+ module Rosetta
2
+ class ApplicationJob < ActiveJob::Base
3
+ end
4
+ end
@@ -0,0 +1,6 @@
1
+ module Rosetta
2
+ class ApplicationMailer < ActionMailer::Base
3
+ default from: "from@example.com"
4
+ layout "mailer"
5
+ end
6
+ end
@@ -0,0 +1,5 @@
1
+ module Rosetta
2
+ class ApplicationRecord < ActiveRecord::Base
3
+ self.abstract_class = true
4
+ end
5
+ 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,6 @@
1
+ module Rosetta
2
+ class Translation < ApplicationRecord
3
+ belongs_to :locale, class_name: "Rosetta::Locale", inverse_of: :translations
4
+ belongs_to :translation_key, class_name: "Rosetta::TranslationKey", inverse_of: :translations
5
+ end
6
+ 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,7 @@
1
+ <% if flash[:notice] %>
2
+ <div class="bg-green-50 border-b border-green-100 text-green-800">
3
+ <div class="mx-auto max-w-4xl p-4 sm:px-6 lg:px-8">
4
+ <%= flash[:notice] %>
5
+ </div>
6
+ </div>
7
+ <% end %>
@@ -0,0 +1,7 @@
1
+ <nav class="border-b border-gray-200 bg-white">
2
+ <div class="mx-auto max-w-4xl px-4 sm:px-6 lg:px-8">
3
+ <div class="flex items-center">
4
+ <%= link_to "Rosetta", root_path, class: "text-xl font-bold" %>
5
+ </div>
6
+ </div>
7
+ </nav>
@@ -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,4 @@
1
+ <%= within_modal do %>
2
+ <%= modal_title "Add a new locale" %>
3
+ <%= render "form", locale: @locale %>
4
+ <% end %>
@@ -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 %>