summaryrefslogtreecommitdiff
path: root/app/controllers
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/controllers
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/controllers')
-rw-r--r--app/controllers/nodes_controller.rb12
-rw-r--r--app/controllers/page_translations_controller.rb98
2 files changed, 107 insertions, 3 deletions
diff --git a/app/controllers/nodes_controller.rb b/app/controllers/nodes_controller.rb
index c1468be..249602d 100644
--- a/app/controllers/nodes_controller.rb
+++ b/app/controllers/nodes_controller.rb
@@ -16,6 +16,8 @@ class NodesController < ApplicationController
16 :revert 16 :revert
17 ] 17 ]
18 18
19 around_action :pin_to_default_locale, :only => [:show, :edit, :update, :autosave]
20
19 def index 21 def index
20 @nodes = Node.root.descendants.includes(:head, :draft) 22 @nodes = Node.root.descendants.includes(:head, :draft)
21 .order('id DESC') 23 .order('id DESC')
@@ -59,9 +61,9 @@ class NodesController < ApplicationController
59 end 61 end
60 62
61 def show 63 def show
62 node = Node.find(params[:id]) 64 @page = @node.draft || @node.head
63 node.wipe_draft! 65 @default_translation = @page.translations.find_by(:locale => I18n.default_locale)
64 @page = node.draft || node.head 66 @translations = @page.translation_summary
65 end 67 end
66 68
67 def edit 69 def edit
@@ -248,6 +250,10 @@ class NodesController < ApplicationController
248 end 250 end
249 end 251 end
250 252
253 def pin_to_default_locale
254 Globalize.with_locale(I18n.default_locale) { yield }
255 end
256
251 def descendant_counts_for(ordered_with_level) 257 def descendant_counts_for(ordered_with_level)
252 counts = Hash.new(0) 258 counts = Hash.new(0)
253 stack = [] # [node, level, index] 259 stack = [] # [node, level, index]
diff --git a/app/controllers/page_translations_controller.rb b/app/controllers/page_translations_controller.rb
new file mode 100644
index 0000000..47f01f7
--- /dev/null
+++ b/app/controllers/page_translations_controller.rb
@@ -0,0 +1,98 @@
1class PageTranslationsController < ApplicationController
2 layout 'admin'
3
4 before_action :login_required
5 before_action :find_node
6 before_action :find_locale, :only => [:show, :edit, :update, :destroy]
7
8 def index
9 page = @node.draft || @node.head
10 @translations = page ? page.translation_summary : []
11 end
12
13 def show
14 @page = @node.draft || @node.head
15 @translation = @page.translations.find_by(:locale => @locale)
16 @default_translation = @page.translations.find_by(:locale => I18n.default_locale)
17 end
18
19 def edit
20 @node.lock_for_editing!(current_user)
21 @page = @node.draft || @node.head
22 @translation = @page.translations.find_by(:locale => @locale)
23 rescue LockedByAnotherUser => e
24 flash[:error] = e.message
25 redirect_to node_path(@node)
26 end
27
28 def update
29 draft = ensure_editable_draft
30 Globalize.with_locale(@locale) { draft.update!(translation_params) }
31 flash[:notice] = "#{@locale.upcase} translation saved. Publish the draft to make it live."
32
33 if params[:commit] == "Save + Unlock + Exit"
34 @node.unlock!
35 redirect_to node_path(@node)
36 else
37 redirect_to edit_node_translation_path(@node, @locale)
38 end
39 rescue LockedByAnotherUser => e
40 flash[:error] = e.message
41 redirect_to node_path(@node)
42 end
43
44 def destroy
45 base = @node.draft || @node.head
46 unless base && base.translated_locales.include?(@locale)
47 flash[:error] = "No #{@locale.to_s.upcase} translation exists to remove."
48 return redirect_to node_path(@node)
49 end
50
51 if (base.translated_locales - [@locale]).empty?
52 flash[:error] = "Can't remove the only remaining translation."
53 return redirect_to node_path(@node)
54 end
55
56 draft = ensure_editable_draft
57 draft.translations.where(:locale => @locale).delete_all
58 draft.reload
59
60 flash[:notice] = "#{@locale.upcase} translation removed from the draft. Publish to make this permanent."
61 redirect_to node_path(@node)
62 rescue LockedByAnotherUser => e
63 flash[:error] = e.message
64 redirect_to node_path(@node)
65 end
66
67 private
68
69 def find_node
70 @node = Node.find(params[:node_id])
71 end
72
73 # Every remaining action's :translation_locale is already
74 # router-constrained to non-default locales, so this should never
75 # actually fire in practice -- it's a deliberate second check, kept
76 # as a drift detector in case the route constraint and this list
77 # (both driven by Page.non_default_locales) ever disagree.
78 def find_locale
79 candidate = params[:translation_locale].to_s.to_sym
80 unless Page.non_default_locales.include?(candidate)
81 raise ActionController::RoutingError, "Unknown or default locale"
82 end
83 @locale = candidate
84 end
85
86 # Never mutates head directly. Locks first (same guard as the
87 # primary editor), then reuses an existing draft or creates one --
88 # deliberately not going through the autosave buffer: this is a
89 # plain, explicit Save, not a live-typing surface.
90 def ensure_editable_draft
91 @node.lock_for_editing!(current_user)
92 @node.draft || @node.create_new_draft(current_user)
93 end
94
95 def translation_params
96 params.fetch(:page, {}).permit(:title, :abstract, :body)
97 end
98end