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/controllers | |
| 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/controllers')
| -rw-r--r-- | app/controllers/nodes_controller.rb | 12 | ||||
| -rw-r--r-- | app/controllers/page_translations_controller.rb | 98 |
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 @@ | |||
| 1 | class 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 | ||
| 98 | end | ||
