# Localization: editor header, permissions, publishing Board: RenewCMS Screens Localization. Frame: LocaleFrame. Data: locale-fixtures.js (single fixture source), tables in locale-spec.js. UI copy says "article", never "story". No revision numbers, IDs or HTTP status names in user-facing text. ## L5 Editor: header, actions and title Route: /article/{id}/edit (one id per version) API: proposed: GET /v1/article/{id} → translations[], capabilities[] Header priority and the action matrix are in the tables below. One filled primary button at most: Save and publish when allowed, dirty and offline; otherwise Save when dirty; otherwise Publish when clean and offline. Save stays separate and secondary. Desktop collapse order as width shrinks (1440 → 1024): reference button loses its label, Unpublish moves to the overflow menu, the freshness badge becomes icon-only. Mobile: title, language, publication and dirty badges stay at the top; Save, primary and an overflow button sit in a fixed bottom bar; reference, Unpublish, Edit slug, Move and Delete go to the overflow sheet. The language control is a menu button with a menu (not a listbox). | ID | State | Trigger | Selector | Sizes | Fixture | |---|---|---|---|---|---| | loc.l5.default | Clean draft | Open a draft you can publish; title pristine | LocaleFrame{s:{v:"ed",x:"default"}} | 1440, 1024, 390 | Journal | | loc.l5.dirty | Dirty draft | Can edit, can't publish: Save is primary | LocaleFrame{s:{v:"ed",x:"dirty"}} | 1440, 1024, 390 | Journal | | loc.l5.dirtyPublish | Dirty draft, publish allowed | Save and publish French is primary; Save secondary | LocaleFrame{s:{v:"ed",x:"dirtyPublish"}} | 1440, 1024, 390 | Journal | | loc.l5.cleanOnline | Clean online | Unpublish only; no primary | LocaleFrame{s:{v:"ed",x:"cleanOnline"}} | 1440, 1024, 390 | Journal | | loc.l5.onlineDirty | Dirty online | Save updates the live page; Unpublish stays | LocaleFrame{s:{v:"ed",x:"onlineDirty"}} | 1440, 1024, 390 | Journal | | loc.l5.saving | Saving | Save in flight | LocaleFrame{s:{v:"ed",x:"saving"}} | 1440, 390 | Journal | | loc.l5.saved | Saved | Toast | LocaleFrame{s:{v:"ed",x:"saved"}} | 1440, 390 | Journal | | loc.l5.saveFail | Save failed | Stays unsaved; Try saving again; nothing published | LocaleFrame{s:{v:"ed",x:"saveFail"}} | 1440, 1024, 390 | Journal | | loc.l5.pubFail | Publish failed after save | Saved as a draft; Retry publish; edits shown as saved | LocaleFrame{s:{v:"ed",x:"pubFail"}} | 1440, 1024, 390 | Journal | | loc.l5.ovf | Overflow menu | More actions (sheet on mobile) | LocaleFrame{s:{v:"ed",x:"ovf"}} | 1440, 390 | Journal | | loc.l5.menu | Menu: four languages | Open menu | LocaleFrame{s:{v:"ed",x:"menu"}} | 1440, 390 | Journal | | loc.l5.two | Menu: two languages | Two-language site | LocaleFrame{s:{v:"ed",x:"two"}} | 1440, 390 | Journal | | loc.l5.many | Menu: many languages | Scrolls inside the viewport, with filter | LocaleFrame{s:{v:"ed",x:"many"}} | 1440, 390 | Journal | | loc.l5.unsaved | Switch with unsaved changes | Choose English while dirty | LocaleFrame{s:{v:"ed",x:"unsaved"}} | 1440, 390 | Journal | | loc.l5.loading | Loading version | Slow switch | LocaleFrame{s:{v:"ed",x:"loading"}} | 1440, 390 | Journal | | loc.l5.navFail | Switch failed | Stays on current version | LocaleFrame{s:{v:"ed",x:"navFail"}} | 1440, 390 | Journal | | loc.l5.gone | Version unavailable | Deleted or no access (neutral) | LocaleFrame{s:{v:"ed",x:"gone"}} | 1440, 390 | Journal | | loc.l5.focus | Title: focus | Field focused, not changed | LocaleFrame{s:{v:"ed",x:"focus"}} | 1440, 390 | Journal | | loc.l5.titleEdit | Title: editing | Typing; counter shown | LocaleFrame{s:{v:"ed",x:"titleEdit"}} | 1440, 390 | Journal | | loc.l5.titleUnsaved | Title: unsaved | Changed, focus left | LocaleFrame{s:{v:"ed",x:"titleUnsaved"}} | 1440, 390 | Journal | | loc.l5.invalid | Title: invalid | Empty: "Enter a title" | LocaleFrame{s:{v:"ed",x:"invalid"}} | 1440, 390 | Journal | | loc.l5.titleShort | Title: too short | Under 3 characters | LocaleFrame{s:{v:"ed",x:"titleShort"}} | 1440, 390 | Journal | | loc.l5.titleLong | Title: too long | Over 200 characters | LocaleFrame{s:{v:"ed",x:"titleLong"}} | 1440, 390 | Journal | | loc.l5.titleSaving | Title: saving | Save in flight | LocaleFrame{s:{v:"ed",x:"titleSaving"}} | 1440, 390 | Journal | | loc.l5.failed | Title: save failed | Text kept, still unsaved, Try again | LocaleFrame{s:{v:"ed",x:"failed"}} | 1440, 390 | Journal | | loc.l5.menuDisabled | Language menu with a disabled language | Spanish is disabled: Disabled tag, version stays readable | LocaleFrame{s:{v:"ed",x:"menuDisabled"}} | 1440, 390 | Journal | ## L5b Permission states (per version) Route: /article/{id}/edit API: capabilities[] on GET /v1/article/{id}; every PATCH/DELETE rechecked on the server Every action renders from its own capability (read, edit, publish, delete, create translation, review). See the capability matrix. Missing-translation rows appear only for users who can create; unreadable siblings are absent with no placeholder and no count. The formatting toolbar is hidden whenever edit is missing. | ID | State | Trigger | Selector | Sizes | Fixture | |---|---|---|---|---|---| | loc.perm.readonly | P1 Read-only version | Banner, read-only fields, no Save, Publish, Delete, toolbar; reference stays | LocaleFrame{s:{v:"perm",x:"ro"}} | 1440, 1024, 390 | Journal | | loc.l5b.roMenu | P1 menu | No Add translation row | LocaleFrame{s:{v:"perm",x:"roMenu"}} | 1440, 390 | Journal | | loc.perm.editNoPublish | P2 Edit, no publish | Save only; hint about the publish permission | LocaleFrame{s:{v:"perm",x:"p2"}} | 1440, 1024, 390 | Journal | | loc.perm.publishNoEdit | P3 Publish, no edit | Publish only; fields read-only | LocaleFrame{s:{v:"perm",x:"p3"}} | 1440, 1024, 390 | Journal | | loc.perm.siblingReadOnly | P4 Sibling readable, not editable | Arabic row tagged Read only | LocaleFrame{s:{v:"perm",x:"p4"}} | 1440, 390 | Journal | | loc.perm.siblingHidden | P5 Sibling not readable | German absent, no count, no placeholder | LocaleFrame{s:{v:"perm",x:"p5"}} | 1440, 390 | Journal | | loc.perm.createNotAll | P6 Can create, not modify all | Delete article and Move disabled with the reason; Delete French version available | LocaleFrame{s:{v:"perm",x:"p6"}} | 1440, 390 | Journal | | loc.l5b.noAdd | Menu without Add translation | No create permission; Arabic is missing but offers nothing | LocaleFrame{s:{v:"perm",x:"noAdd"}} | 1440, 390 | Journal | | loc.l5b.withAdd | Menu with Add translation | Same article, user may create | LocaleFrame{s:{v:"perm",x:"withAdd"}} | 1440, 390 | Journal | ## L8 Publishing and deletion Route: same as the editor API: existing PATCH /v1/article/{id}/publish and /unpublish and DELETE per article id Every action names its language. No bulk publish. With unsaved edits the primary reads Save and publish and saves first; if the save fails nothing is published and the edits stay unsaved; if the save works and publishing fails the edits show as saved. Unpublish never saves: the saved version goes offline and local edits stay in the editor. Saving an online version updates the live page immediately, and the editor says so. Deletion confirmations name only readable versions. | ID | State | Trigger | Selector | Sizes | Fixture | |---|---|---|---|---|---| | loc.l8.publish | Publish French | Confirm, English untouched | LocaleFrame{s:{v:"pd",x:"publish"}} | 1440, 390 | Journal | | loc.lang.publishDisabled | Publishing blocked: language disabled | French disabled on the site | LocaleFrame{s:{v:"pd",x:"publishDisabled"}} | 1440, 390 | Journal | | loc.l8.saveAndPublish | Save and publish French | Unsaved edits: saves first, then publishes | LocaleFrame{s:{v:"pd",x:"saveAndPublish"}} | 1440, 390 | Journal | | loc.l8.saveFailPublish | Save failed, nothing published | Error banner above the editor and an error toast; Save failed badge, primary Try saving again, edits kept, nothing published | LocaleFrame{s:{v:"pd",x:"saveFailPublish"}} | 1440, 390 | Journal | | loc.l8.unpublish | Unpublish French | Confirm | LocaleFrame{s:{v:"pd",x:"unpublish"}} | 1440, 390 | Journal | | loc.l8.unpublishDirty | Unpublish with unsaved edits | Saved version goes offline; edits kept; nothing saved | LocaleFrame{s:{v:"pd",x:"unpublishDirty"}} | 1440, 390 | Journal | | loc.l8.delete | Delete French version | Confirm | LocaleFrame{s:{v:"pd",x:"delete"}} | 1440, 390 | Journal | | loc.l8.deleteSource | Delete a source version | Names the readable versions that track the source; warns review tracking pauses | LocaleFrame{s:{v:"pd",x:"deleteSource"}} | 1440, 390 | Journal | | loc.l8.deleteLast | Delete the only version | Deletes the article | LocaleFrame{s:{v:"pd",x:"deleteLast"}} | 1440, 390 | Journal | | loc.l8.deleteStoryOk | Delete article (all readable versions) | Names readable versions only | LocaleFrame{s:{v:"pd",x:"deleteStoryOk"}} | 1440, 390 | Journal | | loc.l8.deleteStory | Delete article blocked | Neutral, no version named | LocaleFrame{s:{v:"pd",x:"deleteStory"}} | 1440, 390 | Journal | | loc.l8.done | Published | Toast "French published" and the online banner; badge Published, status Saved | LocaleFrame{s:{v:"pd",x:"done"}} | 1440, 390 | Journal | | loc.l8.fail | Publish failed | Toast | LocaleFrame{s:{v:"pd",x:"fail"}} | 1440, 390 | Journal | | loc.l8.deleteDone | French version deleted | Toast; editor opens English | LocaleFrame{s:{v:"pd",x:"deleteDone"}} | 1440, 390 | Journal | | loc.l8.deleteStoryFail | Delete article failed | Dialog error; nothing deleted | LocaleFrame{s:{v:"pd",x:"deleteStoryFail"}} | 1440, 390 | Journal | | loc.l8.deleteStoryDone | Article deleted | Toast on the Articles list | LocaleFrame{s:{v:"li",x:"deleteStoryDone"}} | 1440, 390 | Journal |