diff options
| author | erdgeist <erdgeist@erdgeist.org> | 2026-07-13 16:36:38 +0200 |
|---|---|---|
| committer | erdgeist <erdgeist@erdgeist.org> | 2026-07-13 16:36:38 +0200 |
| commit | 0b6ae26f3b4c3c8278e569fa6644a544e996c25d (patch) | |
| tree | 6c0e8dd23fa243e6bf7e6473aebae3f13ea20dea /app/views | |
| parent | e2705119ba718d40ff4f29c00027c2dfecef01ce (diff) | |
Add per-node translation management: list, edit, destroy, compare
Replaces the old locale-switch-and-edit-the-same-screen workflow,
which conflated presentation locale with content locale and let an
editor silently drift into editing the wrong language with no
persistent signal that anything had changed. Non-default-locale
content now has its own explicit routes and screens, never sharing
a route param with the ambient chrome locale.
- PageTranslationsController: index/show/edit/update/destroy,
scoped to Page.non_default_locales; update handles first-time
creation too, so there's no separate new/create step.
- Reads go through the actual PageTranslation row
(Page#translation_summary), never through the Globalize
fallback-bearing accessor -- fallback is correct for public
rendering but wrong for editing, where a missing translation
needs to look empty, not borrowed from another locale.
- Translations ride on the page's own draft/head cycle; no
independent publish state.
- nodes#show gains a Translations section (per-locale Lock+Edit /
Create+Lock, Destroy, a link into the read-only Compare view) and
a locale indicator on its own default-locale content; nodes#edit,
nodes#update, and nodes#autosave are pinned to the default locale
via Globalize.with_locale regardless of the ambient route locale.
- nodes#show no longer double-loads the node or calls wipe_draft!
on every view (see previous commit for why that's now safe).
- .preview_link_row is renamed .aligned_action_row now that it has
a second real consumer.
Diffstat (limited to 'app/views')
| -rw-r--r-- | app/views/nodes/show.html.erb | 49 | ||||
| -rw-r--r-- | app/views/page_translations/edit.html.erb | 39 | ||||
| -rw-r--r-- | app/views/page_translations/index.html.erb | 19 | ||||
| -rw-r--r-- | app/views/page_translations/show.html.erb | 27 |
4 files changed, 129 insertions, 5 deletions
diff --git a/app/views/nodes/show.html.erb b/app/views/nodes/show.html.erb index 54bb38f..e6a922b 100644 --- a/app/views/nodes/show.html.erb +++ b/app/views/nodes/show.html.erb | |||
| @@ -1,6 +1,6 @@ | |||
| 1 | <% locked_by_other = @node.locked? && @node.lock_owner != current_user %> | 1 | <% locked_by_other = @node.locked? && @node.lock_owner != current_user %> |
| 2 | <div id="page_editor" class="show_node"> | 2 | <div id="page_editor" class="show_node"> |
| 3 | <h1><%= title_for_node(@node) %></h1> | 3 | <h1><%= title_for_node(@node) %> <small>(<%= I18n.default_locale.to_s.upcase %>)</small></h1> |
| 4 | <div id="content"> | 4 | <div id="content"> |
| 5 | <div class="node_description">Status</div> | 5 | <div class="node_description">Status</div> |
| 6 | <div class="node_content node_info_group node_status"> | 6 | <div class="node_content node_info_group node_status"> |
| @@ -70,6 +70,46 @@ | |||
| 70 | <% end %> | 70 | <% end %> |
| 71 | </div> | 71 | </div> |
| 72 | 72 | ||
| 73 | <div class="node_description">Translations</div> | ||
| 74 | <div class="node_content node_info_group"> | ||
| 75 | <div class="node_info_group_items"> | ||
| 76 | <div class="node_info_item"> | ||
| 77 | <span class="node_info_label"><%= I18n.default_locale.to_s.upcase %> (default)</span> | ||
| 78 | <%= @page.title %> — updated <%= @default_translation&.updated_at || @page.updated_at %> | ||
| 79 | </div> | ||
| 80 | <% @translations.each do |t| %> | ||
| 81 | <div class="node_info_item"> | ||
| 82 | <span class="node_info_label"><%= t[:locale].to_s.upcase %></span> | ||
| 83 | <div class="aligned_action_row"> | ||
| 84 | <% if t[:exists] %> | ||
| 85 | <%= link_to t[:title], node_translation_path(@node, t[:locale]) %> — updated <%= t[:updated_at] %> | ||
| 86 | <% if t[:outdated] %> | ||
| 87 | <span class="warning">may be outdated</span> | ||
| 88 | <% end %> | ||
| 89 | <% else %> | ||
| 90 | <span class="field_hint">Not yet translated.</span> | ||
| 91 | <% end %> | ||
| 92 | <% if locked_by_other %> | ||
| 93 | <span class="disabled_action"><%= t[:exists] ? "Lock + Edit" : "Create translation + Lock" %></span> | ||
| 94 | <% else %> | ||
| 95 | <%= link_to edit_node_translation_path(@node, t[:locale]), class: "action_button" do %> | ||
| 96 | <%= icon(t[:exists] ? "edit" : "language", library: "tabler", "aria-hidden": true) %> | ||
| 97 | <%= t[:exists] ? "Lock + Edit" : "Create translation + Lock" %> | ||
| 98 | <% end %> | ||
| 99 | <% if t[:exists] %> | ||
| 100 | <%= button_to node_translation_path(@node, t[:locale]), method: :delete, | ||
| 101 | form: { data: { confirm: "This cannot be undone. Continue?" }, class: 'button_to destructive' } do %> | ||
| 102 | <%= icon("trash", library: "tabler", "aria-hidden": true) %> | ||
| 103 | Destroy Translation | ||
| 104 | <% end %> | ||
| 105 | <% end %> | ||
| 106 | <% end %> | ||
| 107 | </div> | ||
| 108 | </div> | ||
| 109 | <% end %> | ||
| 110 | </div> | ||
| 111 | </div> | ||
| 112 | |||
| 73 | <div class="node_description">People</div> | 113 | <div class="node_description">People</div> |
| 74 | <div class="node_content node_info_group"> | 114 | <div class="node_content node_info_group"> |
| 75 | <div class="node_info_group_items"> | 115 | <div class="node_info_group_items"> |
| @@ -122,7 +162,7 @@ | |||
| 122 | <div class="node_info_item"> | 162 | <div class="node_info_item"> |
| 123 | <span class="node_info_label">Public Preview</span> | 163 | <span class="node_info_label">Public Preview</span> |
| 124 | <% if @node.draft.preview_token.present? %> | 164 | <% if @node.draft.preview_token.present? %> |
| 125 | <div class="preview_link_row"> | 165 | <div class="aligned_action_row"> |
| 126 | <%= link_to shared_preview_path(token: @node.draft.preview_token), shared_preview_url(token: @node.draft.preview_token), target: "_blank", rel: "noopener" %> | 166 | <%= link_to shared_preview_path(token: @node.draft.preview_token), shared_preview_url(token: @node.draft.preview_token), target: "_blank", rel: "noopener" %> |
| 127 | <%= button_to 'Revoke', revoke_shared_preview_node_path(@node), method: :put, form: { data: { confirm: "Revoke this preview link? Anyone currently using it will immediately lose access." }, class: 'button_to state_changing' } %> | 167 | <%= button_to 'Revoke', revoke_shared_preview_node_path(@node), method: :put, form: { data: { confirm: "Revoke this preview link? Anyone currently using it will immediately lose access." }, class: 'button_to state_changing' } %> |
| 128 | </div> | 168 | </div> |
| @@ -205,12 +245,11 @@ | |||
| 205 | </div> | 245 | </div> |
| 206 | <% end %> | 246 | <% end %> |
| 207 | 247 | ||
| 208 | <div class="node_description">Abstract</div> | 248 | <div class="node_description">Abstract (<%= I18n.default_locale.to_s.upcase %>)</div> |
| 209 | <div class="node_content"><%= sanitize(@page.abstract) %></div> | 249 | <div class="node_content"><%= sanitize(@page.abstract) %></div> |
| 210 | 250 | ||
| 211 | <div class="node_description">Body</div> | 251 | <div class="node_description">Body (<%= I18n.default_locale.to_s.upcase %>)</div> |
| 212 | <div class="node_content"><%= sanitize(@page.body) %></div> | 252 | <div class="node_content"><%= sanitize(@page.body) %></div> |
| 213 | <%# Assets not yet addressed - no confirmed model/association seen for this page's attached assets %> | ||
| 214 | 253 | ||
| 215 | </div> | 254 | </div> |
| 216 | </div> | 255 | </div> |
diff --git a/app/views/page_translations/edit.html.erb b/app/views/page_translations/edit.html.erb new file mode 100644 index 0000000..7371a42 --- /dev/null +++ b/app/views/page_translations/edit.html.erb | |||
| @@ -0,0 +1,39 @@ | |||
| 1 | <div class="translation_banner"> | ||
| 2 | Editing the <strong><%= @locale.to_s.upcase %></strong> translation of <strong><%= title_for_node(@node) %></strong>. | ||
| 3 | </div> | ||
| 4 | |||
| 5 | <p class="field_hint"> | ||
| 6 | Metadata such as images, tags, template, and author belong to the page | ||
| 7 | as a whole, not to a single translation — change those from the | ||
| 8 | <%= link_to 'default-locale editor', edit_node_path(@node) %>. | ||
| 9 | </p> | ||
| 10 | |||
| 11 | <div class="node_action_bar"> | ||
| 12 | <%= button_to 'Unlock + Back', unlock_node_path(@node), method: :put, | ||
| 13 | form: { class: 'button_to state_changing' }, | ||
| 14 | disabled: @node.autosave.present? %> | ||
| 15 | <%= submit_tag "Save #{@locale.to_s.upcase} translation", form: "translation_edit_form" %> | ||
| 16 | <%= submit_tag "Save + Unlock + Exit", form: "translation_edit_form" %> | ||
| 17 | <%= link_to "Preview ↗", preview_page_path(@page, :locale => @locale), target: "_blank", rel: "noopener", class: "preview_link" %> | ||
| 18 | </div> | ||
| 19 | |||
| 20 | <div id="page_editor"> | ||
| 21 | <%= form_with url: node_translation_path(@node, @locale), method: :patch, local: true, id: "translation_edit_form" do |f| %> | ||
| 22 | <div id="content"> | ||
| 23 | <div class="node_description">Title</div> | ||
| 24 | <div class="node_content"> | ||
| 25 | <%= text_field_tag "page[title]", @translation&.title %> | ||
| 26 | </div> | ||
| 27 | |||
| 28 | <div class="node_description">Abstract</div> | ||
| 29 | <div class="node_content"> | ||
| 30 | <%= text_area_tag "page[abstract]", @translation&.abstract %> | ||
| 31 | </div> | ||
| 32 | |||
| 33 | <div class="node_description">Body</div> | ||
| 34 | <div class="node_content"> | ||
| 35 | <%= text_area_tag "page[body]", @translation&.body, :class => 'with_editor' %> | ||
| 36 | </div> | ||
| 37 | </div> | ||
| 38 | <% end %> | ||
| 39 | </div> | ||
diff --git a/app/views/page_translations/index.html.erb b/app/views/page_translations/index.html.erb new file mode 100644 index 0000000..a8c4f5d --- /dev/null +++ b/app/views/page_translations/index.html.erb | |||
| @@ -0,0 +1,19 @@ | |||
| 1 | <h1>Translations — <%= title_for_node(@node) %></h1> | ||
| 2 | |||
| 3 | <div class="node_content node_info_group"> | ||
| 4 | <div class="node_info_group_items"> | ||
| 5 | <% @translations.each do |t| %> | ||
| 6 | <div class="node_info_item"> | ||
| 7 | <span class="node_info_label"><%= t[:locale].to_s.upcase %></span> | ||
| 8 | <% if t[:exists] %> | ||
| 9 | <%= t[:title] %> — updated <%= t[:updated_at] %> | ||
| 10 | <% if t[:outdated] %><span class="warning">may be outdated</span><% end %> | ||
| 11 | <%= link_to 'Edit', edit_node_translation_path(@node, t[:locale]) %> | ||
| 12 | <% else %> | ||
| 13 | <span class="field_hint">Not yet translated.</span> | ||
| 14 | <%= link_to 'Add', edit_node_translation_path(@node, t[:locale]) %> | ||
| 15 | <% end %> | ||
| 16 | </div> | ||
| 17 | <% end %> | ||
| 18 | </div> | ||
| 19 | </div> | ||
diff --git a/app/views/page_translations/show.html.erb b/app/views/page_translations/show.html.erb new file mode 100644 index 0000000..c5bd151 --- /dev/null +++ b/app/views/page_translations/show.html.erb | |||
| @@ -0,0 +1,27 @@ | |||
| 1 | <h1>Compare — <%= title_for_node(@node) %></h1> | ||
| 2 | |||
| 3 | <div class="translation_compare"> | ||
| 4 | <div class="translation_compare_column"> | ||
| 5 | <h2><%= @locale.to_s.upcase %></h2> | ||
| 6 | <div class="node_description">Title</div> | ||
| 7 | <div class="node_content"><%= @translation&.title %></div> | ||
| 8 | |||
| 9 | <div class="node_description">Abstract</div> | ||
| 10 | <div class="node_content"><%= sanitize(@translation&.abstract.to_s) %></div> | ||
| 11 | |||
| 12 | <div class="node_description">Body</div> | ||
| 13 | <div class="node_content"><%= sanitize(@translation&.body.to_s) %></div> | ||
| 14 | </div> | ||
| 15 | |||
| 16 | <div class="translation_compare_column"> | ||
| 17 | <h2><%= I18n.default_locale.to_s.upcase %> (default)</h2> | ||
| 18 | <div class="node_description">Title</div> | ||
| 19 | <div class="node_content"><%= @default_translation&.title %></div> | ||
| 20 | |||
| 21 | <div class="node_description">Abstract</div> | ||
| 22 | <div class="node_content"><%= sanitize(@default_translation&.abstract.to_s) %></div> | ||
| 23 | |||
| 24 | <div class="node_description">Body</div> | ||
| 25 | <div class="node_content"><%= sanitize(@default_translation&.body.to_s) %></div> | ||
| 26 | </div> | ||
| 27 | </div> | ||
