chore: redesign article localization before implementation

This commit is contained in:
Florian Sylvain committed 2026-11-01 14:57:07 +01:00
1 parent 99a74dc3a2
commit 8c4a1825ab
31 files changed
+3365 -882

No files matched your search

@@ -1,24 +0,0 @@
# Add a translation
Visual reference: `RenewCMS Screens Localization.dc.html`, group "L4 Add a translation".
Read [localization.md](../localization.md) first. Its integration decisions supersede conflicting board annotations. These states describe target behavior, not shipped behavior.
Route: `/article/{id}/edit?dialog=add-translation&lang=fr`
### Add a translation (group "L4 Add a translation", 8 states)
| # | State | Trigger | Stable ID | Frame selector |
|---|---|---|---|---|
| 1 | Copy from source | Editor → language menu → Add translation | `loc.l4.form` | `at:form` |
| 2 | Start empty | Title only | `loc.l4.empty` | `at:empty` |
| 3 | Creating | Submit in flight | `loc.l4.saving` | `at:saving` |
| 4 | Exists, readable | 409 language_exists, Open offered | `loc.l4.dup` | `at:dup` |
| 5 | Exists, not readable | Neutral 409, no Open | `loc.l4.dupHidden` | `at:dupHidden` |
| 6 | Failed | 5xx, entries kept | `loc.l4.fail` | `at:fail` |
| 7 | Source unavailable | Source deleted or access lost mid-flow | `loc.l4.gone` | `at:gone` |
| 8 | Long dialog | Long title, many authors and images; body scrolls | `loc.l4.long` | `at:long` |
Exact rendered copy and geometry are in `source/LocaleFrame.dc.html` for the selector above. Use Nuxt UI mappings in `components.md`; the frame markup is a visual reference, not production component code.
The standard captures are 1440×900 and 390×844 in light and dark. The narrow reference state also uses 1024×768. The visual harness must support these sources and sizes before this screen can pass its gate. No visual exceptions or new clock-sensitive skips are approved.
@@ -1,27 +0,0 @@
# Article list and coverage
Visual reference: `RenewCMS Screens Localization.dc.html`, group "L7 Article list and coverage".
Read [localization.md](../localization.md) first. Its integration decisions supersede conflicting board annotations. These states describe target behavior, not shipped behavior.
Route: `/articles?site&lang&tab=all|missing|review&q&status&author&seo&page&size`
### Article list and coverage (group "L7 Article list and coverage", 11 states)
| # | State | Trigger | Stable ID | Frame selector |
|---|---|---|---|---|
| 1 | All languages | Stories | `loc.l7.all` | `li:all` |
| 2 | French selected | Versions | `loc.l7.fr` | `li:fr` |
| 3 | Not in French | Missing-translation discovery | `loc.l7.frMissing` | `li:frMissing` |
| 4 | Nothing missing | Separate fixture: Press (3 stories, all translated). Empty; no summary, no pager | `loc.l7.emptyMissing` | `li:emptyMissing` |
| 5 | No results | Search "zinc"; every count 0 | `loc.l7.noResults` | `li:noResults` |
| 6 | Loading | | `loc.l7.frLoading` | `li:frLoading` |
| 7 | Failed | Filters kept | `loc.l7.listFail` | `li:listFail` |
| 8 | Limited access | No dashed missing chips | `loc.l7.limited` | `li:limited` |
| 9 | Mobile filter sheet | Journal, French, Draft: 2 results | `loc.l7.filters` | `li:filters` |
| 10 | Tabs scrolled | Not in French reachable; fade and chevrons | `loc.l7.tabsScroll` | `li:tabsScroll` |
| 11 | Long labels, many languages, filters | Separate fixture: Atelier (8 languages), active filter chips | `loc.l7.long` | `li:long` |
Exact rendered copy and geometry are in `source/LocaleFrame.dc.html` for the selector above. Use Nuxt UI mappings in `components.md`; the frame markup is a visual reference, not production component code.
The standard captures are 1440×900 and 390×844 in light and dark. The narrow reference state also uses 1024×768. The visual harness must support these sources and sizes before this screen can pass its gate. No visual exceptions or new clock-sensitive skips are approved.
@@ -0,0 +1,35 @@
# Localization: conflicts and article moves
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.
## L11 Conflicts and sessions
Route: editor
API: existing 401/403/404/409 patterns
The conflict dialog gives four choices. Keep my changes is the only filled button; the other three are secondary and equal. "Someone else saved this version at <time>". No revision numbers. On mobile the dialog is a bottom sheet with Keep my changes first, so it needs no scrolling at 390 px.
| ID | State | Trigger | Selector | Sizes | Fixture |
|---|---|---|---|---|---|
| loc.l11.session | Session expired | Draft kept locally | LocaleFrame{s:{v:"pm",x:"session"}} | 1440, 390 | Journal |
| loc.l11.restored | Restored after login | Editor shows "Restored unsaved changes from 14:18" | LocaleFrame{s:{v:"pm",x:"restored"}} | 1440, 390 | Journal |
| loc.conflict | Concurrent update | Four choices, local work kept | LocaleFrame{s:{v:"pm",x:"conflict"}} | 1440, 390 | Journal |
| loc.l11.conflictRestored | Concurrent update after session expiry | Restored changes notice above the choices | LocaleFrame{s:{v:"pm",x:"conflictRestored"}} | 1440, 390 | Journal |
| loc.l11.reloadConfirm | Reload theirs: confirm | "This discards your unsaved changes." | LocaleFrame{s:{v:"pm",x:"reloadConfirm"}} | 1440, 390 | Journal |
| loc.l11.conflict2 | Second conflict | Save over theirs failed again; nothing overwritten | LocaleFrame{s:{v:"pm",x:"conflict2"}} | 1440, 390 | Journal |
| loc.l11.download | Download started | Toast; changes still in the editor | LocaleFrame{s:{v:"pm",x:"download"}} | 1440, 390 | Journal |
## L11b Move article to another site
Route: editor → site select
API: proposed: POST /v1/story/{story_id}/move { site }
Entry point is the site select in the editor details. Articles move as a whole, only when the destination enables every language they contain (disabled ones included) and the user can edit all versions. The note lists readable versions only; versions the user can't read are not listed or counted. Moves are atomic and revalidate permissions and conflicts in the transaction.
| ID | State | Trigger | Selector | Sizes | Fixture |
|---|---|---|---|---|---|
| loc.l11b.moveSelect | Site select | Note lists all four versions, one tagged Read only | LocaleFrame{s:{v:"pm",x:"moveSelect"}} | 1440, 390 | Journal |
| loc.l11b.moveSelectHidden | Site select, hidden version | Arabic not readable: not listed or counted | LocaleFrame{s:{v:"pm",x:"moveSelectHidden"}} | 1440, 390 | Journal |
| loc.l11b.moveConfirm | Confirm | Move to Atelier? | LocaleFrame{s:{v:"pm",x:"moveConfirm"}} | 1440, 390 | Journal |
| loc.l11b.moveBusy | In progress | Dialog locked | LocaleFrame{s:{v:"pm",x:"moveBusy"}} | 1440, 390 | Journal |
| loc.l11b.moveDone | Moved | Toast | LocaleFrame{s:{v:"pm",x:"moveDone"}} | 1440, 390 | Journal |
| loc.l11b.move | Language missing in destination | Notes enables English only | LocaleFrame{s:{v:"pm",x:"move"}} | 1440, 390 | Journal, Notes |
| loc.l11b.moveSlug | Address conflict | Names the conflicting readable version only | LocaleFrame{s:{v:"pm",x:"moveSlug"}} | 1440, 390 | Journal, Atelier |
| loc.l11b.movePerm | No permission | Neutral | LocaleFrame{s:{v:"pm",x:"movePerm"}} | 1440, 390 | Journal |
@@ -1,21 +0,0 @@
# Permissions, conflicts, site moves
Visual reference: `RenewCMS Screens Localization.dc.html`, group "L11 Permissions, conflicts, site moves".
Read [localization.md](../localization.md) first. Its integration decisions supersede conflicting board annotations. These states describe target behavior, not shipped behavior.
Route: `various`
### Permissions, conflicts, site moves (group "L11 Permissions, conflicts, site moves", 5 states)
| # | State | Trigger | Stable ID | Frame selector |
|---|---|---|---|---|
| 1 | Session expired | Draft kept locally | `loc.l11.session` | `pm:session` |
| 2 | Concurrent update | Four choices, local work kept | `loc.l11.conflict` | `pm:conflict` |
| 3 | Move: language missing | | `loc.l11.move` | `pm:move` |
| 4 | Move: address conflict | | `loc.l11.moveSlug` | `pm:moveSlug` |
| 5 | Move: no permission | Neutral | `loc.l11.movePerm` | `pm:movePerm` |
Exact rendered copy and geometry are in `source/LocaleFrame.dc.html` for the selector above. Use Nuxt UI mappings in `components.md`; the frame markup is a visual reference, not production component code.
The standard captures are 1440×900 and 390×844 in light and dark. The narrow reference state also uses 1024×768. The visual harness must support these sources and sizes before this screen can pass its gate. No visual exceptions or new clock-sensitive skips are approved.
+69 -27
View File
@@ -1,34 +1,76 @@
# Editor: language navigation
# 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.
Visual reference: `RenewCMS Screens Localization.dc.html`, group "L5 Editor: language navigation".
## L5 Editor: header, actions and title
Route: /article/{id}/edit (one id per version)
API: proposed: GET /v1/article/{id} → translations[], capabilities[]
Read [localization.md](../localization.md) first. Its integration decisions supersede conflicting board annotations. These states describe target behavior, not shipped behavior.
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).
Route: `/article/{id}/edit (one id per version)`
| 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 |
### Editor: language navigation (group "L5 Editor: language navigation", 18 states)
## L5b Permission states (per version)
Route: /article/{id}/edit
API: capabilities[] on GET /v1/article/{id}; every PATCH/DELETE rechecked on the server
| # | State | Trigger | Stable ID | Frame selector |
|---|---|---|---|---|
| 1 | Default (French draft) | Editable title, language menu, badges | `loc.l5.default` | `ed:default` |
| 2 | Menu: four languages | Open menu | `loc.l5.menu` | `ed:menu` |
| 3 | Menu: two languages | Two-language site | `loc.l5.two` | `ed:two` |
| 4 | Menu: many languages | Scrolls inside the viewport, with filter | `loc.l5.many` | `ed:many` |
| 5 | Unsaved changes | Dirty badge; Save enabled | `loc.l5.dirty` | `ed:dirty` |
| 6 | Unsaved draft: Save and publish | Button reads Save and publish French; Save stays separate | `loc.l5.dirtyPublish` | `ed:dirtyPublish` |
| 7 | Unsaved online version | Save updates the live page; Unpublish stays available | `loc.l5.onlineDirty` | `ed:onlineDirty` |
| 8 | Save failed | Stays unsaved; Try saving again; nothing published | `loc.l5.saveFail` | `ed:saveFail` |
| 9 | Switch with unsaved changes | Choose English while dirty | `loc.l5.unsaved` | `ed:unsaved` |
| 10 | Saving | Save in flight | `loc.l5.saving` | `ed:saving` |
| 11 | Saved | Toast | `loc.l5.saved` | `ed:saved` |
| 12 | Loading version | Slow switch | `loc.l5.loading` | `ed:loading` |
| 13 | Switch failed | Stays on current version | `loc.l5.navFail` | `ed:navFail` |
| 14 | Version unavailable | Deleted or no access (neutral) | `loc.l5.gone` | `ed:gone` |
| 15 | Title: editing | Focus | `loc.l5.focus` | `ed:focus` |
| 16 | Title: invalid | Empty | `loc.l5.invalid` | `ed:invalid` |
| 17 | Title: saving | | `loc.l5.titleSaving` | `ed:titleSaving` |
| 18 | Title: save failed | Text kept, still unsaved, Try again | `loc.l5.failed` | `ed:failed` |
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.
Exact rendered copy and geometry are in `source/LocaleFrame.dc.html` for the selector above. Use Nuxt UI mappings in `components.md`; the frame markup is a visual reference, not production component code.
| 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 |
The standard captures are 1440×900 and 390×844 in light and dark. The narrow reference state also uses 1024×768. The visual harness must support these sources and sizes before this screen can pass its gate. No visual exceptions or new clock-sensitive skips are approved.
## 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 | Toast; edits kept and retryable | 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 | Warns about review tracking | 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 | LocaleFrame{s:{v:"pd",x:"done"}} | 1440, 390 | Journal |
| loc.l8.fail | Publish failed | Toast | LocaleFrame{s:{v:"pd",x:"fail"}} | 1440, 390 | Journal |
@@ -1,32 +0,0 @@
# Site languages
Visual reference: `RenewCMS Screens Localization.dc.html`, group "L1 Site languages".
Read [localization.md](../localization.md) first. Its integration decisions supersede conflicting board annotations. These states describe target behavior, not shipped behavior.
Route: `/settings/languages`
### Site languages (group "L1 Site languages", 16 states)
| # | State | Trigger | Stable ID | Frame selector |
|---|---|---|---|---|
| 1 | Single language | Owner opens Languages on Archive (one language, pt-BR kept) | `loc.l1.single` | `sl:single` |
| 2 | Multilingual | Six languages, two disabled (Spanish, Italian) | `loc.l1.multi` | `sl:multi` |
| 3 | Loading | Request in flight | `loc.l1.loading` | `sl:loading` |
| 4 | Load failed | Network or server error | `loc.l1.error` | `sl:error` |
| 5 | Add a language | Add language | `loc.l1.add` | `sl:add` |
| 6 | Add failed | Server error, selection kept | `loc.l1.saveFail` | `sl:saveFail` |
| 7 | Duplicate language | Already enabled | `loc.l1.adddup` | `sl:adddup` |
| 8 | Make default | Row menu | `loc.l1.default` | `sl:default` |
| 9 | Disable with content | Row menu → Disable German | `loc.l1.disable` | `sl:disable` |
| 10 | Disable default blocked | Disable English | `loc.l1.disableDefault` | `sl:disableDefault` |
| 11 | Disable last blocked | Only one enabled | `loc.l1.disableLast` | `sl:disableLast` |
| 12 | Re-enable | Spanish row → Re-enable | `loc.l1.reenable` | `sl:reenable` |
| 13 | Remove (no content) | Italian (0 versions) | `loc.l1.remove` | `sl:remove` |
| 14 | Remove blocked | Spanish has versions | `loc.l1.removeBlocked` | `sl:removeBlocked` |
| 15 | Read-only (403) | Non-owner: list stays readable; no Add language, Re-enable or row menus | `loc.l1.readonly` | `sl:readonly` |
| 16 | Saved | After Add language | `loc.l1.saved` | `sl:saved` |
Exact rendered copy and geometry are in `source/LocaleFrame.dc.html` for the selector above. Use Nuxt UI mappings in `components.md`; the frame markup is a visual reference, not production component code.
The standard captures are 1440×900 and 390×844 in light and dark. The narrow reference state also uses 1024×768. The visual harness must support these sources and sizes before this screen can pass its gate. No visual exceptions or new clock-sensitive skips are approved.
+27
View File
@@ -0,0 +1,27 @@
# Localization: article list and coverage
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.
## L7 Article list and coverage
Route: /articles?site&lang&tab=all|missing|review&q&status&author&seo&page&size
API: proposed: lang, tab=all|missing|review on GET /v1/article/ (same names as the URL)
Journal: 8 articles, 17 versions, page size 5. The list header shows the unit: "8 articles" (all languages, one row per article with its translations), "4 French versions" (language selected), "4 articles not in French" (missing tab), "7 articles need translation", "2 articles need review". Coverage chips: default language first, then other existing versions, then disabled-language versions, then dashed missing chips (only for languages the user can create). Chips collapse after 5 at 1440 and 4 at 1024 and 390, leaving a "+N" chip. Row actions are the same on every width: Open, Open another language, Add translation, Publish or Unpublish, Copy API URL, Delete version; every row has the row menu button. All state is in the URL.
| ID | State | Trigger | Selector | Sizes | Fixture |
|---|---|---|---|---|---|
| loc.l7.all | All languages, page 1 | 5 of 8 articles | LocaleFrame{s:{v:"li",x:"all"}} | 1440, 1024, 390 | Journal |
| loc.l7.page2 | All languages, page 2 (last) | 3 of 8 articles | LocaleFrame{s:{v:"li",x:"page2"}} | 1440, 1024, 390 | Journal |
| loc.l7.fr | French selected | 4 French versions | LocaleFrame{s:{v:"li",x:"fr"}} | 1440, 1024, 390 | Journal |
| loc.l7.frMissing | Not in French | 4 articles not in French | LocaleFrame{s:{v:"li",x:"frMissing"}} | 1440, 1024, 390 | Journal |
| loc.l7.emptyMissing | Nothing missing | Press: all articles translated; no summary, no pager | LocaleFrame{s:{v:"li",x:"emptyMissing"}} | 1440, 1024, 390 | Press |
| loc.l7.noResults | No results | Search "zinc"; every count 0 | LocaleFrame{s:{v:"li",x:"noResults"}} | 1440, 1024, 390 | Journal |
| loc.l7.frLoading | Loading | Counts shown as – | LocaleFrame{s:{v:"li",x:"frLoading"}} | 1440, 1024, 390 | Journal |
| loc.l7.listFail | Failed | Filters kept | LocaleFrame{s:{v:"li",x:"listFail"}} | 1440, 1024, 390 | Journal |
| loc.l7.limited | Limited access | Reads English only, can't create: no dashed chips | LocaleFrame{s:{v:"li",x:"limited"}} | 1440, 1024, 390 | Journal (limited scope) |
| loc.l7.review | Needs review tab | 2 articles need review | LocaleFrame{s:{v:"li",x:"review"}} | 1440, 1024, 390 | Journal |
| loc.l7.author | Author filter | Marta Alves: 3 articles, chips and Clear all | LocaleFrame{s:{v:"li",x:"author"}} | 1440, 1024, 390 | Journal |
| loc.l7.langFilter | Language filter | Enabled and Disabled groups | LocaleFrame{s:{v:"li",x:"langFilter"}} | 1440, 1024, 390 | Journal |
| loc.l7.rowMenu | Row menu | Same actions on desktop and mobile | LocaleFrame{s:{v:"li",x:"rowMenu"}} | 1440, 1024, 390 | Journal |
| loc.l7.filters | Mobile filter sheet | Journal, French, Draft: 2 results | LocaleFrame{s:{v:"li",x:"filters"}} | 1440, 1024, 390 | Journal |
| loc.l7.tabsScroll | Tabs scrolled | Not in French reachable; fade and chevrons | LocaleFrame{s:{v:"li",x:"tabsScroll"}} | 1440, 390 | Journal |
| loc.l7.long | Eight languages, long labels, filters | Atelier: chips collapse to +3, +4, +4; Draft and SEO filters | LocaleFrame{s:{v:"li",x:"long"}} | 1440, 1024, 390 | Atelier |
@@ -0,0 +1,16 @@
# Localization: metadata and public API
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.
## L10 Metadata and public API
Route: Search and sharing panel · /integration
API: existing slug, SEO fields; proposed alternates[], lang; exact lookup by site + lang + slug; resolution by story_id
Slug is unique per (site, lang). Exact lookup is by site + language + slug and never falls back. Language resolution and fallback use the stable story_id. Website URLs come from the site's Public URL pattern; without it the panel says "Public URL not configured" and links to the setting. Canonical is the localized page itself unless an override is set. SEO labels: "Fallback: French title", "No fallback", "Empty. English site default not used."
| ID | State | Trigger | Selector | Sizes | Fixture |
|---|---|---|---|---|---|
| loc.seo.noFallback | Search and sharing: French | Localized slug, canonical, alternates, fallback labels, Edit slug | LocaleFrame{s:{v:"seo",x:""}} | 1440, 390 | Journal |
| loc.slug.published | Edit slug | Published version: warning about old links | LocaleFrame{s:{v:"seo",x:"editSlug"}} | 1440, 390 | Journal |
| loc.url.unset | Public URL not configured | No pattern, no fabricated URL | LocaleFrame{s:{v:"seo",x:"unset"}} | 1440, 390 | Journal |
| loc.l10.noSocial | Social locale not set | fr has no region and none is set: og:locale omitted | LocaleFrame{s:{v:"seo",x:"noSocial"}} | 1440, 390 | Journal |
| loc.l10.api | Integration: languages | Exact lookup, resolve by article group, errors | LocaleFrame{s:{v:"api",x:""}} | 1440, 390 | Journal |
@@ -1,20 +0,0 @@
# Metadata and public API
Visual reference: `RenewCMS Screens Localization.dc.html`, group "L10 Metadata and public API".
Read [localization.md](../localization.md) first. Its integration decisions supersede conflicting board annotations. These states describe target behavior, not shipped behavior.
Route: `Search and sharing panel · /integration`
### Metadata and public API (group "L10 Metadata and public API", 4 states)
| # | State | Trigger | Stable ID | Frame selector |
|---|---|---|---|---|
| 1 | Search and sharing: French | Localized slug, canonical, alternates | `loc.l10.seo` | `seo:` |
| 2 | Public URL not configured | No pattern, no fabricated URL | `loc.l10.unset` | `seo:unset` |
| 3 | Social locale not set | fr has no region and none is configured: og:locale omitted | `loc.l10.noSocial` | `seo:noSocial` |
| 4 | Integration: languages | Proposed requests | `loc.l10.api` | `api:` |
Exact rendered copy and geometry are in `source/LocaleFrame.dc.html` for the selector above. Use Nuxt UI mappings in `components.md`; the frame markup is a visual reference, not production component code.
The standard captures are 1440×900 and 390×844 in light and dark. The narrow reference state also uses 1024×768. The visual harness must support these sources and sizes before this screen can pass its gate. No visual exceptions or new clock-sensitive skips are approved.
+28 -14
View File
@@ -1,20 +1,34 @@
# New article
# Localization: new article and add translation
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.
Visual reference: `RenewCMS Screens Localization.dc.html`, group "L3 New article".
## L3 New article
Route: /articles (New article modal)
API: existing POST /v1/article/ + proposed lang, story_id
Read [localization.md](../localization.md) first. Its integration decisions supersede conflicting board annotations. These states describe target behavior, not shipped behavior.
Single-language sites show no language field. Multilingual sites show Language and a New article / Translation of an existing article choice. The translation path uses the same endpoint and rules as Add translation. The picker lists only articles the user can read; the chosen version is the source.
Route: `/articles (New article modal)`
| ID | State | Trigger | Selector | Sizes | Fixture |
|---|---|---|---|---|---|
| loc.l3.single | Single language | Title and site only | LocaleFrame{s:{v:"na",x:"single"}} | 1440, 390 | Archive |
| loc.l3.multi | Multilingual: new article | Language defaults to the site default | LocaleFrame{s:{v:"na",x:"multi"}} | 1440, 390 | Journal |
| loc.l3.translation | Translation of an article | Article picker + language | LocaleFrame{s:{v:"na",x:"translation"}} | 1440, 390 | Journal |
| loc.l3.sitechange | Site changed | Selected language not offered by the new site | LocaleFrame{s:{v:"na",x:"sitechange"}} | 1440, 390 | Journal, Notes |
### New article (group "L3 New article", 4 states)
## L4 Add a translation
Route: /article/{id}/edit?dialog=add-translation&lang=fr
API: proposed: POST /v1/article/{id}/translations
| # | State | Trigger | Stable ID | Frame selector |
|---|---|---|---|---|
| 1 | Single language | Title and site only | `loc.l3.single` | `na:single` |
| 2 | Multilingual: new story | Language defaults to the site default | `loc.l3.multi` | `na:multi` |
| 3 | Translation of a story | Story picker + language | `loc.l3.translation` | `na:translation` |
| 4 | Site changed | Selected language not offered by new site | `loc.l3.sitechange` | `na:sitechange` |
Always creates a draft. Source: the user picks which readable version to copy from (default: the default-language version if readable, else the first readable). The source version is captured in the same transaction as the copy. Copied text stays in the source language until edited; the version starts as Needs review. Start empty: no tracked source, so freshness shows No source. The primary action stays in the sticky footer on mobile; the body scrolls.
Exact rendered copy and geometry are in `source/LocaleFrame.dc.html` for the selector above. Use Nuxt UI mappings in `components.md`; the frame markup is a visual reference, not production component code.
The standard captures are 1440×900 and 390×844 in light and dark. The narrow reference state also uses 1024×768. The visual harness must support these sources and sizes before this screen can pass its gate. No visual exceptions or new clock-sensitive skips are approved.
| ID | State | Trigger | Selector | Sizes | Fixture |
|---|---|---|---|---|---|
| loc.l4.form | Copy from source | Editor → language menu → Add translation | LocaleFrame{s:{v:"at",x:"form"}} | 1440, 390 | Journal |
| loc.l4.empty | Start empty | Title only | LocaleFrame{s:{v:"at",x:"empty"}} | 1440, 390 | Journal |
| loc.l4.saving | Creating | Submit in flight | LocaleFrame{s:{v:"at",x:"saving"}} | 1440, 390 | Journal |
| loc.dup.readable | Exists, readable | Conflict; Open offered | LocaleFrame{s:{v:"at",x:"dup"}} | 1440, 390 | Journal |
| loc.dup.hidden | Exists, not readable | Neutral error, no Open | LocaleFrame{s:{v:"at",x:"dupHidden"}} | 1440, 390 | Journal |
| loc.l4.fail | Failed | Server error, entries kept | LocaleFrame{s:{v:"at",x:"fail"}} | 1440, 390 | Journal |
| loc.l4.gone | Source deleted | Deleted while the dialog was open | LocaleFrame{s:{v:"at",x:"gone"}} | 1440, 390 | Journal |
| loc.l4.unreadable | Source no longer readable | Access lost between opening and submitting | LocaleFrame{s:{v:"at",x:"unreadable"}} | 1440, 390 | Journal |
| loc.l4.langOff | Language disabled mid-flow | An owner disabled French while the dialog was open | LocaleFrame{s:{v:"at",x:"langOff"}} | 1440, 390 | Journal |
| loc.l4.long | Long dialog | Long title, many authors and images; body scrolls, footer stays | LocaleFrame{s:{v:"at",x:"long"}} | 1440, 390 | Journal |
@@ -1,23 +0,0 @@
# Permission states (per version)
Visual reference: `RenewCMS Screens Localization.dc.html`, group "L5b Permission states (per version)".
Read [localization.md](../localization.md) first. Its integration decisions supersede conflicting board annotations. These states describe target behavior, not shipped behavior.
Route: `/article/{id}/edit`
### Permission states (per version) (group "L5b Permission states (per version)", 7 states)
| # | State | Trigger | Stable ID | Frame selector |
|---|---|---|---|---|
| 1 | Read-only version | No Save, Publish, Delete, toolbar | `loc.l5b.ro` | `ed:ro` |
| 2 | Read-only: menu | No Add translation row | `loc.l5b.roMenu` | `ed:roMenu` |
| 3 | Edit, no publish | Save only | `loc.l5b.p2` | `ed:p2` |
| 4 | Publish, no edit | Publish only; fields read-only | `loc.l5b.p3` | `ed:p3` |
| 5 | Sibling readable, not editable | German tagged Read only | `loc.l5b.p4` | `ed:p4` |
| 6 | Sibling not readable | German absent, no count | `loc.l5b.p5` | `ed:p5` |
| 7 | Can create, not modify all | Story delete and move disabled | `loc.l5b.p6` | `ed:p6` |
Exact rendered copy and geometry are in `source/LocaleFrame.dc.html` for the selector above. Use Nuxt UI mappings in `components.md`; the frame markup is a visual reference, not production component code.
The standard captures are 1440×900 and 390×844 in light and dark. The narrow reference state also uses 1024×768. The visual harness must support these sources and sizes before this screen can pass its gate. No visual exceptions or new clock-sensitive skips are approved.
@@ -1,26 +0,0 @@
# Publishing and deletion
Visual reference: `RenewCMS Screens Localization.dc.html`, group "L8 Publishing and deletion".
Read [localization.md](../localization.md) first. Its integration decisions supersede conflicting board annotations. These states describe target behavior, not shipped behavior.
Route: `same as editor`
### Publishing and deletion (group "L8 Publishing and deletion", 10 states)
| # | State | Trigger | Stable ID | Frame selector |
|---|---|---|---|---|
| 1 | Publish French | Confirm, English untouched | `loc.l8.publish` | `pd:publish` |
| 2 | Save and publish French | Unsaved edits: one action saves first, then publishes | `loc.l8.saveAndPublish` | `pd:saveAndPublish` |
| 3 | Save failed, nothing published | Toast; edits kept and retryable | `loc.l8.saveFailPublish` | `pd:saveFailPublish` |
| 4 | Unpublish French | Confirm | `loc.l8.unpublish` | `pd:unpublish` |
| 5 | Delete French version | Confirm | `loc.l8.delete` | `pd:delete` |
| 6 | Delete a source version | Warns about review reference | `loc.l8.deleteSource` | `pd:deleteSource` |
| 7 | Delete last version | Deletes the story | `loc.l8.deleteLast` | `pd:deleteLast` |
| 8 | Delete story blocked | Neutral, no version named | `loc.l8.deleteStory` | `pd:deleteStory` |
| 9 | Published | Toast | `loc.l8.done` | `pd:done` |
| 10 | Publish failed | Toast | `loc.l8.fail` | `pd:fail` |
Exact rendered copy and geometry are in `source/LocaleFrame.dc.html` for the selector above. Use Nuxt UI mappings in `components.md`; the frame markup is a visual reference, not production component code.
The standard captures are 1440×900 and 390×844 in light and dark. The narrow reference state also uses 1024×768. The visual harness must support these sources and sizes before this screen can pass its gate. No visual exceptions or new clock-sensitive skips are approved.
@@ -1,27 +0,0 @@
# Source reference
Visual reference: `RenewCMS Screens Localization.dc.html`, group "L6 Source reference".
Read [localization.md](../localization.md) first. Its integration decisions supersede conflicting board annotations. These states describe target behavior, not shipped behavior.
Route: `/article/{id}/edit?ref=:sourceId`
### Source reference (group "L6 Source reference", 11 states)
| # | State | Trigger | Stable ID | Frame selector |
|---|---|---|---|---|
| 1 | Side by side | Show source reference | `loc.l6.split` | `src:split` |
| 2 | Reference hidden | Hide source reference; button returns | `loc.l6.closed` | `src:closed` |
| 3 | Reopened | Show source reference again; scroll positions kept | `loc.l6.reopen` | `src:reopen` |
| 4 | Change reference | Menu; tracked source stays English | `loc.l6.changeRef` | `src:changeRef` |
| 5 | Narrow desktop (1024 px) | True 1024 px frame | `loc.l6.narrow` | `src:narrow` |
| 6 | Long content | Both panes scroll independently | `loc.l6.long` | `src:long` |
| 7 | Arabic beside English | dir=rtl on the Arabic title and body only | `loc.l6.rtl` | `src:rtl` |
| 8 | Mobile: editing | Segment | `loc.l6.mobileEdit` | `src:mobileEdit` |
| 9 | Mobile: source | Segment | `loc.l6.mobileSrc` | `src:mobileSrc` |
| 10 | Mobile: closed | Show source reference button | `loc.l6.mobileClosed` | `src:mobileClosed` |
| 11 | Reference unavailable | Deleted or unreadable | `loc.l6.unavail` | `src:unavail` |
Exact rendered copy and geometry are in `source/LocaleFrame.dc.html` for the selector above. Use Nuxt UI mappings in `components.md`; the frame markup is a visual reference, not production component code.
The standard captures are 1440×900 and 390×844 in light and dark. The narrow reference state also uses 1024×768. The visual harness must support these sources and sizes before this screen can pass its gate. No visual exceptions or new clock-sensitive skips are approved.
@@ -0,0 +1,43 @@
# Localization: source reference and review (release 2)
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.
## L6 Source reference (release 2) (release 2)
Route: /article/{id}/edit?ref=:sourceId
API: proposed: GET /v1/article/{id} (read-only fetch of the reference)
Release 2. The temporary reference (?ref=, dashed outline, "Temporary reference") and the tracked review source (solid "Tracked source" badge in the review banner) are separate: changing the reference never changes the tracked source. Show source reference is in the editor header (desktop, icon-only at 1024) and the overflow sheet (mobile). Each pane scrolls independently. dir and lang go on the title and body of the editing pane and of the reference, never on the app shell.
| ID | State | Trigger | Selector | Sizes | Fixture |
|---|---|---|---|---|---|
| loc.l6.split | Side by side | Show source reference | LocaleFrame{s:{v:"src",x:"split"}} | 1440, 1024, 390 | Journal |
| loc.l6.closed | Reference hidden | Hide source reference; button returns | LocaleFrame{s:{v:"src",x:"closed"}} | 1440, 390 | Journal |
| loc.l6.reopen | Reopened | Scroll positions kept | LocaleFrame{s:{v:"src",x:"reopen"}} | 1440, 390 | Journal |
| loc.l6.changeRef | Change reference | Menu; tracked source stays English | LocaleFrame{s:{v:"src",x:"changeRef"}} | 1440, 390 | Journal |
| loc.l6.narrow | Narrow desktop (1024) | True 1024 frame | LocaleFrame{s:{v:"src",x:"narrow"}} | 1024, 390 | Journal |
| loc.l6.long | Long content | Both panes scroll independently | LocaleFrame{s:{v:"src",x:"long"}} | 1440, 390 | Journal |
| loc.l6.rtl | Arabic editing, English reference | dir=rtl on the Arabic title and body only | LocaleFrame{s:{v:"src",x:"rtl"}} | 1440, 390 | Journal |
| loc.l6.arRef | French editing, Arabic reference | dir=rtl on the reference pane only | LocaleFrame{s:{v:"src",x:"arRef"}} | 1440, 390 | Journal |
| loc.l6.mobileEdit | Mobile: editing | Segment | LocaleFrame{s:{v:"src",x:"mobileEdit"}} | 1440, 390 | Journal |
| loc.l6.mobileSrc | Mobile: source | Segment | LocaleFrame{s:{v:"src",x:"mobileSrc"}} | 1440, 390 | Journal |
| loc.l6.mobileClosed | Mobile: closed | Show source reference in the bottom bar or sheet | LocaleFrame{s:{v:"src",x:"mobileClosed"}} | 1440, 390 | Journal |
| loc.l6.unavail | Reference unavailable | Deleted or unreadable | LocaleFrame{s:{v:"src",x:"unavail"}} | 1440, 390 | Journal |
## L9 Freshness and review (release 2) (release 2)
Route: editor banner · list badge
API: proposed: POST /v1/article/{id}/review { source_article_id, source_revision, translation_revision } (internal fields; never shown)
Release 2. Publication (solid badge) and freshness (outlined badge with refresh icon) are separate. Labels: No version, Draft, Needs review, Reviewed. A copied draft starts as Needs review. The dialog shows "Changed fields: Title, Body", the source version with its date and the translation version with its date. No revision numbers appear in the UI.
| ID | State | Trigger | Selector | Sizes | Fixture |
|---|---|---|---|---|---|
| loc.l9.warn | Source updated | English changed after review | LocaleFrame{s:{v:"fr",x:"warn"}} | 1440, 390 | Journal |
| loc.l9.pub | Published, needs review | Still online | LocaleFrame{s:{v:"fr",x:"pub"}} | 1440, 390 | Journal |
| loc.l9.review | Mark reviewed | Confirm with changed fields and dates | LocaleFrame{s:{v:"fr",x:"review"}} | 1440, 390 | Journal |
| loc.review.changed | Source changed again | During the review | LocaleFrame{s:{v:"fr",x:"changed"}} | 1440, 390 | Journal |
| loc.l9.changedTr | Translation changed | Saved by someone else while confirming | LocaleFrame{s:{v:"fr",x:"changedTr"}} | 1440, 390 | Journal |
| loc.l9.replace | Choose tracked source | Cycle prevented | LocaleFrame{s:{v:"fr",x:"replace"}} | 1440, 390 | Journal |
| loc.src.inaccessible | Source unavailable (access) | History kept | LocaleFrame{s:{v:"fr",x:"inaccessible"}} | 1440, 390 | Journal |
| loc.src.deleted | Source deleted | Choose a new source | LocaleFrame{s:{v:"fr",x:"gone"}} | 1440, 390 | Journal |
| loc.l9.done | Reviewed | Toast | LocaleFrame{s:{v:"fr",x:"done"}} | 1440, 390 | Journal |
| loc.l9.fail | Review failed | Toast | LocaleFrame{s:{v:"fr",x:"fail"}} | 1440, 390 | Journal |
| loc.l9.frReview | Published and needs review in the list | French tab, Needs review: 2 versions | LocaleFrame{s:{v:"li",x:"frReview"}} | 1440, 1024, 390 | Journal |
@@ -1,26 +0,0 @@
# Freshness and review
Visual reference: `RenewCMS Screens Localization.dc.html`, group "L9 Freshness and review".
Read [localization.md](../localization.md) first. Its integration decisions supersede conflicting board annotations. These states describe target behavior, not shipped behavior.
Route: `editor banner · list badge`
### Freshness and review (group "L9 Freshness and review", 10 states)
| # | State | Trigger | Stable ID | Frame selector |
|---|---|---|---|---|
| 1 | Source updated | English changed after review | `loc.l9.warn` | `fr:warn` |
| 2 | Published, needs review | | `loc.l9.pub` | `fr:pub` |
| 3 | Mark reviewed | Confirm with revisions | `loc.l9.review` | `fr:review` |
| 4 | Source changed again | Revision 9 while reviewing 8 | `loc.l9.changed` | `fr:changed` |
| 5 | Translation changed | Saved by someone else while confirming | `loc.l9.changedTr` | `fr:changedTr` |
| 6 | Choose tracked source | Cycle prevented | `loc.l9.replace` | `fr:replace` |
| 7 | Source unavailable (access) | History kept | `loc.l9.inaccessible` | `fr:inaccessible` |
| 8 | Source deleted | Replace | `loc.l9.gone` | `fr:gone` |
| 9 | Reviewed | Toast | `loc.l9.done` | `fr:done` |
| 10 | Review failed | Toast | `loc.l9.fail` | `fr:fail` |
Exact rendered copy and geometry are in `source/LocaleFrame.dc.html` for the selector above. Use Nuxt UI mappings in `components.md`; the frame markup is a visual reference, not production component code.
The standard captures are 1440×900 and 390×844 in light and dark. The narrow reference state also uses 1024×768. The visual harness must support these sources and sizes before this screen can pass its gate. No visual exceptions or new clock-sensitive skips are approved.
-21
View File
@@ -1,21 +0,0 @@
# First-time setup for existing articles
Visual reference: `RenewCMS Screens Localization.dc.html`, group "L2 First-time setup for existing articles".
Read [localization.md](../localization.md) first. Its integration decisions supersede conflicting board annotations. These states describe target behavior, not shipped behavior.
Route: `/settings/sites/:key/languages (banner) · /settings/languages/setup`
### First-time setup for existing articles (group "L2 First-time setup for existing articles", 5 states)
| # | State | Trigger | Stable ID | Frame selector |
|---|---|---|---|---|
| 1 | Ready to assign | Journal (en), Notes (no locale, must choose), Archive (pt-BR kept) | `loc.l2.ready` | `su:ready` |
| 2 | Site with no articles | Gallery has no articles; en-GB kept, confirm and Save | `loc.l2.empty` | `su:empty` |
| 3 | Applying | POST in flight | `loc.l2.applying` | `su:applying` |
| 4 | Done | 16 articles labelled | `loc.l2.done` | `su:done` |
| 5 | Failed | Nothing changed; choices kept | `loc.l2.fail` | `su:fail` |
Exact rendered copy and geometry are in `source/LocaleFrame.dc.html` for the selector above. Use Nuxt UI mappings in `components.md`; the frame markup is a visual reference, not production component code.
The standard captures are 1440×900 and 390×844 in light and dark. The narrow reference state also uses 1024×768. The visual harness must support these sources and sizes before this screen can pass its gate. No visual exceptions or new clock-sensitive skips are approved.
@@ -0,0 +1,70 @@
# Localization: site languages, public URL and setup
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.
## L1 Site languages
Route: /settings/sites/:key/languages
API: proposed: GET/PUT /v1/site/{key}/languages
Navigation: Settings → Sites → [site] → Languages only. The site name is in the heading and breadcrumb. Site formatting locale, default content language, article language and social locale are four separate values. At least one language stays enabled and the default must be enabled: to retire the default, first Make default on another language, then Disable. Disable and Remove never delete or unpublish content. Disabled languages show a Disabled tag and a Re-enable action; their versions stay readable and editable per permission. Drafts can be published only after Re-enable ("French is disabled on this site. Re-enable it to publish."). The language filter lists them under Disabled. Social locale per language has three states: Derived (from the language code, e.g. pt-BR gives pt_BR), Set (an owner typed it, e.g. fr_FR) and Not set (og:locale left out). Only owners edit it.
| ID | State | Trigger | Selector | Sizes | Fixture |
|---|---|---|---|---|---|
| loc.l1.single | Single language | Owner opens Languages on Archive (one language, pt-BR kept) | LocaleFrame{s:{v:"sl",x:"single"}} | 1440, 390 | Archive |
| loc.l1.multi | Multilingual | Journal: four enabled, two disabled (Spanish, Italian) | LocaleFrame{s:{v:"sl",x:"multi"}} | 1440, 1024, 390 | Journal |
| loc.l1.loading | Loading | Request in flight | LocaleFrame{s:{v:"sl",x:"loading"}} | 1440, 390 | Journal |
| loc.l1.error | Load failed | Network or server error | LocaleFrame{s:{v:"sl",x:"error"}} | 1440, 390 | Journal |
| loc.l1.add | Add a language | Add language | LocaleFrame{s:{v:"sl",x:"add"}} | 1440, 390 | Journal |
| loc.l1.saveFail | Add failed | Server error, selection kept | LocaleFrame{s:{v:"sl",x:"saveFail"}} | 1440, 390 | Journal |
| loc.l1.adddup | Duplicate language | Already enabled | LocaleFrame{s:{v:"sl",x:"adddup"}} | 1440, 390 | Journal |
| loc.l1.broader | Only a broader tag supported | Add Portuguese (Portugal): substitution explained, confirmation required | LocaleFrame{s:{v:"sl",x:"broader"}} | 1440, 390 | Journal |
| loc.l1.default | Make default | Row menu | LocaleFrame{s:{v:"sl",x:"default"}} | 1440, 390 | Journal |
| loc.l1.defaultFail | Set default failed | Server error, nothing changed | LocaleFrame{s:{v:"sl",x:"defaultFail"}} | 1440, 390 | Journal |
| loc.l1.disable | Disable with content | Row menu → Disable German | LocaleFrame{s:{v:"sl",x:"disable"}} | 1440, 390 | Journal |
| loc.l1.stale | Languages changed | Another owner changed them: "Languages changed. Reload." | LocaleFrame{s:{v:"sl",x:"stale"}} | 1440, 390 | Journal |
| loc.lang.disableDefault | Disable default blocked | Disable English | LocaleFrame{s:{v:"sl",x:"disableDefault"}} | 1440, 390 | Journal |
| loc.lang.disableLast | Disable last blocked | Only one enabled | LocaleFrame{s:{v:"sl",x:"disableLast"}} | 1440, 390 | Journal |
| loc.l1.reenable | Re-enable | Spanish row → Re-enable | LocaleFrame{s:{v:"sl",x:"reenable"}} | 1440, 390 | Journal |
| loc.l1.reenableDone | Re-enabled | Toast after success | LocaleFrame{s:{v:"sl",x:"reenableDone"}} | 1440, 390 | Journal |
| loc.l1.reenableFail | Re-enable failed | Dialog error, still disabled | LocaleFrame{s:{v:"sl",x:"reenableFail"}} | 1440, 390 | Journal |
| loc.l1.remove | Remove (no content) | Italian (0 versions) | LocaleFrame{s:{v:"sl",x:"remove"}} | 1440, 390 | Journal |
| loc.l1.removeFail | Remove failed | Dialog error, still listed | LocaleFrame{s:{v:"sl",x:"removeFail"}} | 1440, 390 | Journal |
| loc.l1.removeBlocked | Remove blocked | Spanish has versions | LocaleFrame{s:{v:"sl",x:"removeBlocked"}} | 1440, 390 | Journal |
| loc.l1.readonly | Read-only view | Non-owner: list and social locale readable; no controls | LocaleFrame{s:{v:"sl",x:"readonly"}} | 1440, 390 | Journal |
| loc.l1.saved | Saved | After Add language | LocaleFrame{s:{v:"sl",x:"saved"}} | 1440, 390 | Journal |
| loc.l1.social | Social locale: three states | Atelier: Derived en_GB and pt_BR, Set fr_FR, Not set | LocaleFrame{s:{v:"sl",x:"social"}} | 1440, 1024, 390 | Atelier |
| loc.l1.socialEdit | Social locale: edit | Edit on French (Set: fr_FR) | LocaleFrame{s:{v:"sl",x:"socialEdit"}} | 1440, 390 | Atelier |
| loc.l1.socialInvalid | Social locale: invalid | fr-FR typed with a hyphen | LocaleFrame{s:{v:"sl",x:"socialInvalid"}} | 1440, 390 | Atelier |
## L1b Public URL pattern (owner setting)
Route: /settings/sites/:key/public-url
API: proposed: GET/PUT /v1/site/{key} { public_url_pattern }
Owner-only. One pattern per site, for example https://example.com/{lang}/{slug}. Supported placeholders: {lang} (language code) and {slug} (the article address in that language). {slug} is required: without it every article would share one address. The preview uses a real article of the site. When unset, article panels show "Public URL not configured" and link here. No URL is invented.
| ID | State | Trigger | Selector | Sizes | Fixture |
|---|---|---|---|---|---|
| loc.l1b.set | Set | Pattern saved, preview shown | LocaleFrame{s:{v:"pu",x:"set"}} | 1440, 390 | Journal |
| loc.l1b.editing | Editing | Live preview follows the field | LocaleFrame{s:{v:"pu",x:"editing"}} | 1440, 390 | Journal |
| loc.l1b.unknown | Unknown placeholder | {language} typed | LocaleFrame{s:{v:"pu",x:"unknown"}} | 1440, 390 | Journal |
| loc.l1b.scheme | Bad scheme | ftp:// typed | LocaleFrame{s:{v:"pu",x:"scheme"}} | 1440, 390 | Journal |
| loc.l1b.encoded | Not encoded | A space in the path | LocaleFrame{s:{v:"pu",x:"encoded"}} | 1440, 390 | Journal |
| loc.l1b.noSlug | Missing {slug} | Pattern without {slug} | LocaleFrame{s:{v:"pu",x:"noSlug"}} | 1440, 390 | Journal |
| loc.l1b.saved | Saved | Toast | LocaleFrame{s:{v:"pu",x:"saved"}} | 1440, 390 | Journal |
| loc.l1b.unset | Unset | No pattern; opened from "Public URL not configured" | LocaleFrame{s:{v:"pu",x:"unset"}} | 1440, 390 | Journal |
| loc.l1b.viewOnly | Read-only view | Non-owner: pattern readable, no controls | LocaleFrame{s:{v:"pu",x:"viewOnly"}} | 1440, 390 | Journal |
## L2 First-time setup for existing articles
Route: /settings/sites/:key/languages/setup
API: proposed: GET /v1/language/setup, POST /v1/language/setup { sites:[{ key, lang }] } (lang is the full tag, e.g. pt-BR)
Opened from the Languages banner of any site; :key is the site it was opened from, and the page lists every site that still has unlabelled articles. Setup assigns a language per site and applies in one transaction. It never translates, links or moves articles. Each site is seeded from its full Site.Locale when a matching language entry exists: pt-BR becomes Portuguese (Brazil). A site with no locale has no seed and the owner must choose. Nothing applies until the owner confirms every row, including a site with no articles. Retrying is safe.
| ID | State | Trigger | Selector | Sizes | Fixture |
|---|---|---|---|---|---|
| loc.l2.ready | Ready to assign | Journal (en), Notes (no locale, must choose), Archive (pt-BR kept) | LocaleFrame{s:{v:"su",x:"ready"}} | 1440, 390 | Journal, Notes, Archive |
| loc.l2.noLocale | No-locale site | Notes only: the owner must choose | LocaleFrame{s:{v:"su",x:"noLocale"}} | 1440, 390 | Notes |
| loc.l2.empty | Site with no articles | Gallery has no articles; en-GB kept, confirm and Save | LocaleFrame{s:{v:"su",x:"empty"}} | 1440, 390 | Gallery |
| loc.l2.applying | Applying | Request in flight | LocaleFrame{s:{v:"su",x:"applying"}} | 1440, 390 | Journal, Notes, Archive |
| loc.l2.inProgress | Setup already running | Another owner started it; starting again is safe | LocaleFrame{s:{v:"su",x:"inProgress"}} | 1440, 390 | Journal, Notes, Archive |
| loc.l2.done | Done | 16 articles labelled | LocaleFrame{s:{v:"su",x:"done"}} | 1440, 390 | Journal, Notes, Archive |
| loc.l2.fail | Failed | Nothing changed; choices kept; retry is safe | LocaleFrame{s:{v:"su",x:"fail"}} | 1440, 390 | Journal, Notes, Archive |
@@ -0,0 +1,19 @@
# Localization: existing surfaces
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.
## L12 Existing surfaces
Route: Home · Activity · Media · editor details
API: existing; language added where it applies
Single-language sites show none of this. Home counts articles, not versions.
| ID | State | Trigger | Selector | Sizes | Fixture |
|---|---|---|---|---|---|
| loc.l12.home | Home: recent articles | Default-language title; code only when not default | LocaleFrame{s:{v:"sf",x:"home"}} | 1440, 1024, 390 | Journal |
| loc.l12.homeFr | Home: reader of French only | Titles in French with code FR | LocaleFrame{s:{v:"sf",x:"homeFr"}} | 1440, 1024, 390 | Journal |
| loc.l12.activity | Activity | Events name the language | LocaleFrame{s:{v:"sf",x:"activity"}} | 1440, 1024, 390 | Journal |
| loc.l12.media | Media usage | Shared image counted once, versions listed | LocaleFrame{s:{v:"sf",x:"media"}} | 1440, 1024, 390 | Journal |
| loc.l12.menu | Article menu labels | Publish and Delete name the language | LocaleFrame{s:{v:"sf",x:"menu"}} | 1440, 390 | Journal |
| loc.l12.apiCopy | API URL copy | Copies the version URL with ?lang= | LocaleFrame{s:{v:"sf",x:"apiCopy"}} | 1440, 390 | Journal |
| loc.l12.seoPreview | SEO preview | Localized address and canonical | LocaleFrame{s:{v:"sf",x:"seoPreview"}} | 1440, 390 | Journal |
| loc.l12.siteFallback | Site text fallback | Used only when its language matches the article | LocaleFrame{s:{v:"sf",x:"siteFallback"}} | 1440, 390 | Journal |