summaryrefslogtreecommitdiff
path: root/app/views
diff options
context:
space:
mode:
authorerdgeist <erdgeist@erdgeist.org>2026-07-13 16:36:38 +0200
committererdgeist <erdgeist@erdgeist.org>2026-07-13 16:36:38 +0200
commit0b6ae26f3b4c3c8278e569fa6644a544e996c25d (patch)
tree6c0e8dd23fa243e6bf7e6473aebae3f13ea20dea /app/views
parente2705119ba718d40ff4f29c00027c2dfecef01ce (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.erb49
-rw-r--r--app/views/page_translations/edit.html.erb39
-rw-r--r--app/views/page_translations/index.html.erb19
-rw-r--r--app/views/page_translations/show.html.erb27
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>