diff --git a/docs/design/README.md b/docs/design/README.md index 9e35584..7854fb2 100644 --- a/docs/design/README.md +++ b/docs/design/README.md @@ -35,6 +35,6 @@ Every state has a desktop and a mobile frame, and the theme switch gives light a - The Back-office board still shows the owner, admin and user navigation. Writer frames use the restricted `user` navigation of `AppFrame`. - The Schema board group 8 (Permissions) is superseded by the role editor, and `/settings/permissions` redirects to `/settings/roles`. -## Article localization baseline (2026-10-31) +## Article localization baseline (refreshed 2026-11-01) -Start with [localization.md](localization.md), then [localization-tables.md](localization-tables.md). Target states are indexed in `screens/localization-*.md`. New references: `source/RenewCMS Screens Localization.dc.html`, `LocaleFrame.dc.html`, `locale-spec.js`, and `LocalizationShell.dc.html`. The shell is isolated so existing boards retain their original AppFrame reference. This imports design artifacts only; no production behavior or visual gate has been verified. The localization handoff supersedes older title immutability and per-site slug rules only for slices that explicitly accept those changes. +Start with [localization.md](localization.md) (authority, integration rules, open decisions), then [localization-tables.md](localization-tables.md) (generated from `source/locale-spec.js`) and [localization-corrections.md](localization-corrections.md) (what the last design pass rendered and left open). States are indexed in `states.md` and `screens/localization-*.md` (8 files, 169 states in 15 groups; L6 and L9 are release 2). References: `source/RenewCMS Screens Localization.dc.html`, `LocaleFrame.dc.html`, `locale-spec.js`, `locale-fixtures.js` and `LocalizationShell.dc.html`. The shell is isolated so existing boards keep their original AppFrame. The LocaleLive prototype was not imported. This is a target design; no production behavior or visual gate has been verified. diff --git a/docs/design/components.md b/docs/design/components.md index 2b3412e..03113c9 100644 --- a/docs/design/components.md +++ b/docs/design/components.md @@ -169,22 +169,23 @@ Accessibility of the tri-state cell: | BlockedDialog | New | UModal with dismissible false, no close button; UButton primary for the next step | Profile | Specs: field heights 40 (44 mobile), buttons 36 (44 mobile), card padding 16, section gap 16. Colors from tokens only. Destructive confirm is disabled at 40% opacity until valid. Implemented in AccountCards.dc.html and the AppFrame modal. -## Localization components - -All new components use existing theme tokens, square corners, 32px desktop and 44px mobile action targets unless the referenced frame specifies otherwise. Read `localization.md` and the matching `screens/localization-*.md` first. - -| Component | Built from | Props, slots and responsive behavior | +## Localization components (spec) +| Component | Built from (Nuxt UI v4) | Notes | |---|---|---| -| LanguageMenu | UButton + UDropdownMenu, UBadge, UIcon | Readable versions and allowed creation actions; one menu item per action, no nested buttons; bounded scrolling; full language names and selected marker; keyboard focus returns to trigger | -| LocalizedTitle | UForm + UFormField + UInput | Per-version value, language, direction, validation and dirty state; readonly text without update permission; title remains 3–200 characters | -| TranslationDialog | UModal + UForm + UFormField + USelectMenu + UInput + URadioGroup + UAlert + UButton | Language, readable source, title, empty/copy choice; scrollable body; mobile bottom-sheet geometry; preserve entries on errors | -| SourcePane | Custom pane using UButton, UDropdownMenu, UIcon | Readonly article HTML; dir/lang on content; independent overflow; mobile source/edit switch; closing preserves draft and scroll | -| FreshnessBadge | UBadge variant="outline" + refresh icon | Separate from publication badge; text communicates state without color alone | -| ReviewBanner | UAlert + UButton | Capability-gated review, reference and tracked-source actions; unavailable/deleted source states | -| MobileFilterSheet | USlideover side="bottom" + UFormField + USelectMenu + UButton | Site, language, status, author, SEO; apply/clear; active removable chips; URL persistence | -| LocaleTabs | UTabs + UButton + UIcon | Bounded horizontal scrolling, edge cues and named scroll buttons; active tab reachable on keyboard and touch | -| SiteLanguageList | UTable desktop, responsive cards mobile; UDropdownMenu + UBadge | Owner-only mutations; readable metadata without mutation controls; default/disabled markers | -| LocalePager | Existing AppPaginator | Page/size and explicit story/version units; retain existing allowed sizes | -| LocalizationConfirm | Existing confirm dialog using UModal, UAlert, UButton | Language-specific scope; loading/failure; no disclosure of unreadable siblings | - -Capability checks belong in existing access helpers/computed state. A generic CapabilityGate framework is not required. Reuse existing typed API client, toast, modal, media and editor components rather than reproducing the mockup helper implementations. +| LanguageMenu | UDropdownMenu with a UButton trigger | Menu button with menu; rows carry badges and a Read only tag; no Add translation row without create permission | +| CapabilityGate | none (logic) | Renders each action from capabilities[]; never from the role name | +| EditorHeader | UButton, UBadge, UDropdownMenu | Title row, then language, badges and actions. At most one filled button. Collapse order and mobile bottom bar in the header priority table | +| TitleInput | UInput (heading size) | Label "Title (French)"; bounds 3 to 200; counter; helper under the field | +| ActionOverflow | UDropdownMenu (desktop), UDrawer (mobile) | Disabled items carry a visible reason | +| StatusBadge | UBadge variant soft | Publication: solid icon plus word | +| FreshnessBadge | UBadge variant outline | Refresh icon plus word; icon-only at 1024 with aria-label | +| SourcePane | UCard | Read-only reference with own scroll and dir/lang. Dashed outline and "Temporary reference" tag | +| TrackedSourceBadge | UBadge variant soft color info | Distinct from the temporary reference | +| ConflictDialog | UModal (desktop), UDrawer (mobile) | Four options; Keep my changes is the only filled button | +| CoverageChips | UBadge variant outline | Collapse after 5 (1440) or 4 (1024, 390) with a +N chip; dashed for missing | +| ListFilters | USelectMenu, UInput, UDrawer on mobile | Language filter groups Enabled and Disabled | +| UnitLabel | plain text, aria-live polite | "8 articles", "4 French versions", "4 articles not in French" | +| RowMenu | UDropdownMenu / UDrawer | Same actions on every width | +| SocialLocaleField | UInput, UBadge | Derived, Set, Not set | +| PublicUrlPattern | UInput, UAlert | Live preview from a real article, validation messages | +| MoveArticlePanel | USelectMenu, UModal | Note lists readable versions only | diff --git a/docs/design/localization-corrections.md b/docs/design/localization-corrections.md new file mode 100644 index 0000000..c44cacc --- /dev/null +++ b/docs/design/localization-corrections.md @@ -0,0 +1,53 @@ +# Localization corrections and completion report + +Board: RenewCMS Screens Localization. Frame: LocaleFrame. Fixtures: locale-fixtures.js. Spec: locale-spec.js. LocaleLive and the Handoff board were not touched. + +## State counts +169 states in 15 groups (was 109). L1 25, L1b 9, L2 7, L3 4, L4 10, L5 25, L5b 9, L6 12 (release 2), L7 16, L8 13, L9 11 (release 2), L10 5, L11 7, L11b 8, L12 8. 33 states are also drawn at 1024. The full ID table is in states.md, and the board lists IDs, selectors and fixtures per state. + +IDs: 99 existing states keep their group, frame and selector. 70 are new. Ten existing states were rekeyed without changing meaning: the seven permission states moved from frame ed to perm (L5b), and the three move failures moved from L11 to L11b. Legacy IDs (loc.perm.*, loc.dup.*, loc.lang.*, loc.src.*, loc.review.changed, loc.conflict, loc.slug.published, loc.url.unset, loc.seo.noFallback) were kept as the canonical ID of the state they map to; the loc.lN.xxx alias is listed beside each. Nothing was removed without a replacement. + +## Sections to files and states +| Section | Files | State IDs | +|---|---|---| +| 1 Permissions | LocaleFrame, locale-spec.js | loc.perm.readonly, loc.l5b.roMenu, loc.perm.editNoPublish, loc.perm.publishNoEdit, loc.perm.siblingReadOnly, loc.perm.siblingHidden, loc.perm.createNotAll, loc.l5b.noAdd, loc.l5b.withAdd, loc.l5.cleanOnline | +| 2 Header, actions, title | LocaleFrame, AppFrame (edHeader, edMobBar slots), spec tables Editor header priority, Action matrix, Title field states | loc.l5.default, dirty, dirtyPublish, cleanOnline, onlineDirty, saving, saved, saveFail, pubFail, ovf, focus, titleEdit, titleUnsaved, invalid, titleShort, titleLong, titleSaving, failed; loc.l8.unpublishDirty | +| 3 Conflict | LocaleFrame | loc.conflict, loc.l11.conflictRestored, reloadConfirm, conflict2, download, restored, session | +| 4 List | LocaleFrame, fixtures view() | loc.l7.all, page2, fr, frMissing, emptyMissing, noResults, frLoading, listFail, limited, review, author, langFilter, rowMenu, filters, tabsScroll, long | +| 5 Languages and setup | LocaleFrame, routes.md | loc.l1.* (reenableDone, reenableFail, broader, defaultFail, removeFail, stale, social, socialEdit, socialInvalid, readonly), loc.l1b.*, loc.l2.noLocale, inProgress | +| 6 Move, delete, add translation | LocaleFrame | loc.l11b.*, loc.l8.deleteStoryOk, deleteStory, loc.l4.long, unreadable, langOff | +| 7 Review and reference (release 2) | LocaleFrame | loc.l9.*, loc.l6.* (arRef, narrow at 1024, rtl) | +| 8 Metadata, API, surfaces | LocaleFrame | loc.l10.*, loc.l12.* | +| 9 Fixtures | locale-fixtures.js, board tables Fixture index and Fixture totals check | all | +| 10 Accessibility | spec table Accessibility and interaction, board table Contrast | all | +| 11 Docs | routes.md, states.md, components.md, README.md, screens/localization-*.md (8 files, generated from the spec) | all | + +## Rendered and inspected +I rendered the frame at full size and looked at these. Everything else is specified in the spec and the board, and drawn by the same code, but I did not look at it. +- 1440 light: ed.dirtyPublish, perm.ro, perm.p6, ed.menu, ed.titleLong, pd.unpublishDirty, pm.conflict, pm.moveSelect, fr.review, li.all, li.long, li.author, li.limited, sl.social, sl.broader, su.ready, pu.encoded, seo (default), api, sf.home, src.split. +- 1024: ed.dirtyPublish (dark), ed.cleanOnline, src.rtl (dark), li.long, li.all. +- 390: ed.dirtyPublish, ed.pubFail, ed.ovf, pm.conflict (all four options and the primary visible without scrolling), at.long (primary stays at the bottom), li.filters, li.long, li.rowMenu, li.page2, src.mobileSrc, sl.multi (dark), perm.ro (dark). +- Dark: li.all 1440, ed.dirtyPublish 1024, src.rtl 1024, sl.multi 390, perm.ro 390. +- The board's computed tables (fixture totals, state counts, contrast) were read in the browser. Every total matches the fixture. Contrast passes for every badge, chip and selected variant in light and dark. Disabled controls at 40% (2.6:1 light) are exempt. + +## Not completed +- Most states were not looked at. Only the list above was rendered and inspected. 1024 frames exist for 33 states and I looked at five. +- Icons come from an online icon service and did not load in my screenshots, so icon presence and placement were not checked visually. +- Header widths between 1024 and 1440 were not drawn; the collapse order is specified and shown at the two ends. +- The mobile Languages rows are taller than needed (social locale wraps onto its own line). Not tuned. +- Nuxt UI components are named in components.md; the frames are built from the design tokens, as before. +- Archive, Notes and Gallery appear in single states, not as full lists. +- Setup frames still list all sites, although the route is per site (see below). +- LocaleLive and the Handoff board still describe the old behaviour. + +## Documents that disagreed, and what I aligned to +- Route: routes.md and the board said /settings/languages; the brief says /settings/sites/:key/languages. Aligned to the brief. The setup page lists every site, so :key is the site it was opened from. +- Wording: the brief says "story" never appears in the UI, but section 1 uses "Delete story" and "You can't delete this story." I used "article" everywhere ("Delete article", "You can't delete this article."). Rule not changed; only the word. +- Count: the brief's example "5 French versions" disagrees with the decided fixture (fr 4). Aligned to the fixture: "4 French versions". +- routes.md still listed list params mode and needs_review and the old public route GET /v1/public/site/{key}/article/{slug}?lang=. Removed; aligned to tab and the two final public routes. +- states.md said the frame was not yet updated for the permission states; replaced by the generated table. +- The previous 1024 frames were a 1440 frame clipped to 1024. AppFrame now takes a width, so 1024 frames are real. +- Section 2 asks for a "Retry publish" action; the banner and the primary both use that label. + +## Rules I believe are wrong +None changed. One to watch: Save over theirs sits at equal weight with Reload theirs, as required. If both stay unfilled, a user may not see that Save over theirs is the destructive one. The explanation text carries that. Left as is. diff --git a/docs/design/localization-implementation-prompt.md b/docs/design/localization-implementation-prompt.md deleted file mode 100644 index 88fe696..0000000 --- a/docs/design/localization-implementation-prompt.md +++ /dev/null @@ -1,55 +0,0 @@ -# Fresh-context implementation prompt - -Paste the following into a new Codex chat in the RenewCMS workspace. - -```text -Implement article localization in RenewCMS using our Claude Code /slice workflow, adapted to your available agent tools. Act as orchestrator; do not implement the whole feature in one coder pass. - -Read first: -- CLAUDE.md and CODING_STANDARDS.md. -- .claude/commands/slice.md. -- .claude/agents/specifier.md, coder.md, cleaner.md, architect.md, hardener.md, qa.md. -- Any applicable AGENTS.md instructions. -- docs/design/README.md and docs/design/localization.md. -- docs/design/localization-tables.md, relevant screens/localization-*.md, routes.md, states.md, components.md. - -The user authorized importing the latest Claude Design localization output into docs/design in the previous chat. That import is complete. docs/design/localization.md records corrections and precedence, including compatibility, direct saving of online content, locale handling, conflict semantics and limitations. Treat it as the integration handoff. It does not mean Gherkin scenarios have been accepted. - -The imported visual sources are: -- docs/design/source/RenewCMS Screens Localization.dc.html -- docs/design/source/LocaleFrame.dc.html -- docs/design/source/locale-spec.js -- docs/design/source/LocalizationShell.dc.html - -LocalizationShell isolates the exported shell from older boards; production must reuse the existing SPA shell. Existing shared design boards were not overwritten. The connected localization prototype is unfinished and imported frames have not been visually validated. Do not claim visual parity or waive visual gates. New screen files enumerate 109 target states across 12 groups. The visual checker may need explicit source/group/viewport integration as a separately scoped change. - -Use fresh subagents for stages. Run dependent stages strictly sequentially and hand off actual accepted scenario text, paths, exact changed-file scope and prior reports. Claude's Agent/TodoWrite/AskUserQuestion/Read/Bash names should be mapped to available equivalents. Do not assume Sonnet/Haiku models or PushNotification exist here. Keep reports on disk under a task-specific tmp directory. Report unavailable tools honestly. - -Start by inspecting the current repository and relevant existing feature pairs. Propose a concise slice roadmap grounded in the handoff, then run specifier for the first bounded foundational slice. Choose a useful complete unit, not all localization storage, UI and review at once. - -Present the first slice's Gherkin and companion QA draft for explicit acceptance. Do not write accepted feature files or start coder before acceptance; this is required by .claude/commands/slice.md and .claude/agents/specifier.md. Explain that source when presenting the approval step. After acceptance, verify the promised files exist under features/ before proceeding. - -Then follow /slice faithfully: -- coder uses red/green/refactor and acceptance tests. -- Measure changed lines, files and layer crossings; select fast/full path. -- cleaner and architect only under the documented gates; architect is read-only. -- One scoped mutation pass owned by the appropriate stage, through task mutate only; hardener reuses its results on the full path. -- hardener verifies boundaries, tests, scenario fidelity, and required visual conformance. -- qa audits final handoffs and accepted-scenario coverage and runs required final checks. -- Follow documented retry limits and report blockers rather than inventing a PASS. - -Preserve these constraints: -- Git read-only; no commits, checkout, reset or branch writes. -- No source comments except required tool directives. -- No secret, .env or real database reads. -- Dependency direction stays api → useCases → domain ← persistence/mailer. -- Existing API shapes and anonymous/public article listing behavior stay compatible unless a slice explicitly accepts a change. -- Regional locales are preserved; language setup never translates or guesses groups. -- Every version has independent permissions and publishing; unreadable siblings stay undisclosed. -- Saving an online version updates live content under existing update permission; no unpublished-revision workflow is being added. -- Optional reference language and tracked review source are separate. -- Use existing Nuxt UI components, typed API client and design tokens. -- docs/design is read-only during implementation stages. Surface remaining contradictions during specification and obtain a concrete design decision; do not improvise in coder or grant visual exceptions yourself. - -Do not ask me to restate the feature or return to Claude Design. Use the imported handoff and repository. Ask only for material unresolved product decisions or the explicit scenario acceptance required by our workflow. -``` diff --git a/docs/design/localization-tables.md b/docs/design/localization-tables.md index 13f3fd3..5a8cb69 100644 --- a/docs/design/localization-tables.md +++ b/docs/design/localization-tables.md @@ -1,6 +1,6 @@ # Localization board tables -Imported from `source/locale-spec.js`. Read `localization.md` first: its explicit corrections supersede conflicting text below. API additions are proposals to be specified and accepted through `/slice`, not existing endpoints. +Generated from `source/locale-spec.js`. Read `localization.md` first: its explicit corrections supersede conflicting text below. API additions are proposals to be specified and accepted through `/slice`, not existing endpoints. ## Decisions @@ -23,7 +23,7 @@ Imported from `source/locale-spec.js`. Read `localization.md` first: its explici | List: language selected | One row per version; Not in French tab lists stories | Units always stated | Tabs and counts | lang, tab params | Initial | | Search | All languages: every accessible title. Language: that language only | Predictable | Search | Access-filtered | Initial | | Publishing | Per version; copy always names the language; no bulk publish | Prevents accidental scope | Publish button copy | None | Initial | -| Deleting | Per version. Last version deletes the story. Story delete needs permission on all | Explicit scope | Delete dialog | None | Initial | +| Deleting | Per version. Last version deletes the story. Article delete needs permission on all | Explicit scope | Delete dialog | None | Initial | | Freshness | Needs review when the source revision changes after the reviewed revision; Mark as reviewed records it | Reliable, not timestamp-based | None | content_revision on versions; reviewed_source_revision | Initial | | Revision-bumping fields | Title, body, excerpt, slug, image alt text, SEO and social text. Not authors, cover, site, publication | Only translatable content counts | None | Revision incremented in same write | Initial | | Site move | Moves the whole story; destination must enable every language; slug conflicts block | No splitting or merging | Site select in editor | Move endpoint, validation | Initial | @@ -36,17 +36,22 @@ Imported from `source/locale-spec.js`. Read `localization.md` first: its explici | Missing vs hidden | "Not in French" lists stories where French is absent or unreadable to the user. Add French translation on a story with an unreadable French returns a neutral 409 (see Visibility policy) | Discovery cannot leak existence | List | Conflict check ignores read permission; response is neutral | Initial | | Source-copy access | Read access to the chosen source is rechecked at submit, inside the transaction | Access can change while the dialog is open | Add translation | Neutral 403/404 | Initial | | Disabled languages | Versions stay readable and editable; publishing blocked until re-enabled; filter lists them under Disabled | No silent unpublishing | Site languages | enabled flag per site language | Initial | +| Header priority | Left to right: title input (own row), language dropdown, publication badge, status badge (unsaved, saving, saved, failed), freshness badge, Read only tag; then reference button, Unpublish, Save, primary, overflow. At most one filled button. Collapse order: reference label, Unpublish to overflow, freshness to icon | The primary action is never hidden | Editor header | None | Initial | +| Public URL pattern | Per-site owner setting with {lang} and {slug}; live preview from a real article; unset state links from "Public URL not configured" | No fabricated website URLs | Settings → Sites | public_url_pattern on site | Initial | +| Social locale per language | Per-language owner field on Languages: Derived (from the tag), Set (typed), Not set. Format language_REGION | Never guess a region | Languages page | social_locale per site language | Initial | +| Release split | Source reference (L6) and Freshness and review (L9) are release 2. Frames are drawn in full so they can be checked | Smaller first release | None | None | Release 2 | +| UI wording | The UI says "article", never "story". No revision numbers, IDs or HTTP status names in user-facing copy. Internal notes may say story | Plain language | All localization copy | None | Initial | ## Capability matrix (per version, enforced server-side) | Case | Read | Edit | Publish | Delete | Create translation | Review | UI result | | --- | --- | --- | --- | --- | --- | --- | --- | -| P1 Read-only version | Yes | No | No | No | If can create on site | No | Banner "You can read this version. You can't edit it." No Save, Publish, Delete; formatting toolbar hidden; fields read-only; source reference still available | +| P1 Read-only version | Yes | No | No | No | If can create on site | No | Banner "You can read this version. You can't edit it." No Save, Publish, Delete; formatting toolbar hidden; fields read-only; the reference action remains | | P2 Edit, no publish | Yes | Yes | No | Per scope | Per scope | If can edit | Save shown; Publish hidden; hint "Publishing needs the publish permission" | | P3 Publish, no edit | Yes | No | Yes | Per scope | Per scope | No | Publish/Unpublish shown, fields read-only (only if existing permissions allow publish without edit) | | P4 Sibling readable, not editable | Yes | No | No | No | n/a | n/a | Opens read-only; row tag "Read only" | | P5 Sibling not readable | No | No | No | No | n/a | n/a | Row absent; no count, no placeholder | -| P6 Can create, not modify all | Yes | Own versions | Own | Own | Yes | Own | Story delete and move disabled: "You can't delete this story." Delete this version stays available if allowed | +| P6 Can create, not modify all | Yes | Own versions | Own | Own | Yes | Own | Delete article and Move article disabled with the reason "You can't delete this article." Delete French version stays available if allowed | | Owner / Admin | Yes | Yes | Yes | Yes | Yes | Yes | All readable versions; not inferred from role label alone | ## Field ownership, copy and fallback @@ -69,31 +74,41 @@ Imported from `source/locale-spec.js`. Read `localization.md` first: its explici | Capability | Status | Request | Errors | | --- | --- | --- | --- | -| List articles | Existing, extended | GET /v1/article/?page=&size=&q=&status=&site=&seo= (existing). Proposed: lang, tab=all / missing / review (same names as the URL; no separate mode or needs_review) | 400 invalid page/size (SPA never shows it) | -| Get article | Existing, extended | GET /v1/article/{id}. Proposed fields: lang, story_id, capabilities[], translations[] (readable only), source { article_id, revision, status: tracked / inaccessible / deleted } | 404 for hidden or deleted | +| List articles | Existing, extended | GET /v1/article/?page=&size=&q=&status=&site=&seo= (existing). Proposed: lang, tab=all\|missing\|review (same names as the URL; no separate mode or needs_review) | 400 invalid page/size (SPA never shows it) | +| Get article | Existing, extended | GET /v1/article/{id}. Proposed fields: lang, story_id, capabilities[], translations[] (readable only), source { article_id, revision, status: tracked\|inaccessible\|deleted } | 404 for hidden or deleted | | Create article | Existing, extended | POST /v1/article/. Proposed: lang, story_id, source_article_id, copy (routes to translation rules) | 409 language_unavailable · 422 language_not_enabled | | Edit | Existing, extended | PATCH /v1/article/{id}. Proposed: if_revision for overwrite recheck | 409 revision_conflict { current_revision } | | Publish / unpublish | Existing | PATCH /v1/article/{id}/publish, /unpublish. Per version | 403 missing_permission · 422 language_disabled | | Delete | Existing | DELETE /v1/article/{id}. Last version deletes the story | 403 · 404 | | Add translation | Proposed | POST /v1/article/{id}/translations { lang, title, source_article_id, copy }. Path id is any readable version; source_article_id selects the copy source; source_revision captured in the transaction | 409 language_unavailable (neutral) · 404 (neutral) · 422 · 403 | | Mark reviewed | Proposed | POST /v1/article/{id}/review { source_article_id, source_revision, translation_revision } | 409 source_changed · translation_changed · source_replaced · 403 | -| Change tracked source | Proposed | PATCH /v1/article/{id}/source { source_article_id / null } | 422 source_cycle · 422 source_other_story · 403 | -| Site languages | Proposed | GET/PUT /v1/site/{key}/languages { default, languages[{ lang, enabled }] }, atomic | 422 default_not_enabled · no_enabled_language · 409 language_in_use | -| Setup | Proposed | GET/POST /v1/language/setup { sites:[{ key, lang }] }, idempotent per site key; articles created during setup get the site default; retry after failure is safe | 403 · 409 setup_in_progress | +| Change tracked source | Proposed | PATCH /v1/article/{id}/source { source_article_id\|null } | 422 source_cycle · 422 source_other_story · 403 | +| Site languages | Proposed | GET/PUT /v1/site/{key}/languages { default, languages[{ lang, enabled }] }, atomic | 422 default_not_enabled · no_enabled_language · 409 language_in_use · 409 languages_changed | +| Setup | Proposed | GET/POST /v1/language/setup { sites:[{ key, lang }] }, idempotent per site key; articles created during setup get the site default; retry after failure is safe. UI route /settings/sites/:key/languages/setup | 403 · 409 setup_in_progress | | Move story | Proposed | POST /v1/story/{story_id}/move { site }, atomic, revalidates | 409 slug_conflict · 422 language_not_enabled · 403 (neutral) | | Publish with unsaved edits | Client sequence | PATCH /v1/article/{id} (save) then PATCH /v1/article/{id}/publish. The second call is sent only if the first returns 200. No new endpoint | Save failure: nothing published, edits stay unsaved | | Public article (two requests) | Proposed | Exact: GET /v1/public/site/{key}/lang/{lang}/article/{slug}. Resolve: GET /v1/public/site/{key}/story/{story_id}?lang=&fallback=default. Existing article-ID links unchanged | See Public lookup | +| Public URL pattern | Proposed | GET/PUT /v1/site/{key} { public_url_pattern }. Placeholders {lang}, {slug}. Owner only | 422 unknown_placeholder · invalid_scheme · not_encoded · missing_slug · 403 | +| Social locale | Proposed | PUT /v1/site/{key}/languages { languages[{ lang, enabled, social_locale\|null }] } | 422 invalid_social_locale | ## Accessibility and interaction | Control | Accessible name and keyboard | Focus | Announcement | | --- | --- | --- | --- | -| Language control | "Language: French. Switch version". Enter or Down opens a menu; arrows move; Enter selects; Escape closes | Into list on open; back to control on close | Polite: "Editing French version" | +| Language control | "Language: French. Switch version". Enter or Down opens a menu; arrows move; Enter selects; Escape closes | Into the list on open; back to the control on close | Polite: "Editing French version" | +| Title input | Label "Title (French)"; aria-describedby points to the help or error text; invalid sets aria-invalid and the message is an alert | Stays in the field; on Try again, back to the field | Polite: "Saving title", "Title saved"; errors are announced | +| Save, Save and publish, Retry publish | Buttons named by their text including the language ("Save and publish French"); busy state sets aria-busy; Tab order: title, language, status, reference, Unpublish, Save, primary, More actions | After save, stays on the button; after a failed save, on Try saving again | Polite: "French saved"; "Saved as a draft. Publishing failed. Try publishing again." | +| More actions menu or sheet | Menu button "More actions"; items are menuitems; disabled items use aria-disabled and show the reason as text | First enabled item on open; back to More actions on close | None | +| Reference button | "Show source reference" or "Hide source reference", icon-only at 1024 with the same name | Moves into the pane on show; back to the button on hide | Polite: "Showing English reference" | +| Conflict dialog and sheet | role dialog, modal; options are buttons in order Keep my changes, Download my version, Reload theirs, Save over theirs; Escape equals Keep my changes | First button (Keep my changes) on open; back to the title on close | Polite: "Download started" | +| Move panel and dialogs | Site select is a listbox named "Site"; the note is the description of the confirm button | Into the list; back to the site select | Polite: "Moved to Atelier" | | Add translation dialog | Focus trapped; Escape cancels unless creating | First field; back to the control | Polite: "French draft created" | | Source segment (mobile) | Two-button radiogroup, arrows switch | Stays on the segment | Polite: "Showing English source" | -| Coverage chips | aria-label per chip: "French: draft, needs review" | Row, not chips | None | -| Dialogs on mobile | Bottom sheet, grabber, 44 px targets | As dialogs elsewhere | None | -| Reduced motion | No slide transitions on popover or sheets | | | +| Coverage chips | aria-label per chip: "French: draft, needs review". The "+3" chip is named "3 more languages: …" | Row, not chips. Dashed chips are buttons on desktop only when the user can create | None | +| List unit label | aria-live polite: "4 articles not in French" | None | Polite on every filter change | +| Status without color | Publication badges are solid with an icon (Published: check, Draft: pencil). Freshness badges are outlined with a refresh icon and a word. Save status always has an icon and a word | None | None | +| Touch targets | On mobile every button, menu row, chip button and segment is at least 44 px high; the bottom bar keeps the primary visible | None | None | +| Reduced motion | No slide or fade transitions on popovers, sheets and the tab strip scroll when prefers-reduced-motion is set; the saving spinner becomes a static icon with the word "Saving" | None | None | ## Acceptance scenarios @@ -117,22 +132,23 @@ Imported from `source/locale-spec.js`. Read `localization.md` first: its explici | Item | Note | | --- | --- | -| Home | Recent articles show the default-language title with a language code when not default. No other change. | -| Activity | Events gain the language: "Published French version of …". Existing events unchanged. | -| Article menu | Publish and Delete labels name the language on multilingual sites. | -| API URL copy | Copies the version URL with ?lang= on multilingual sites. | -| SEO preview | Shows the localized slug and canonical. | -| Media usage | Counts a shared image once; lists versions that use it. | -| Verification limit | Repository files (docs/design, docs/api/articles.md, EditorView.vue, ArticleView.vue, NewArticleModal.vue, ArticleDetails.vue, useArticleDraft.ts, articleDomain.go, siteDomain.go, ResolveArticleSEO.go, features/*.feature) were not accessible. Statements about them are assumptions marked verify in repo. The .claude/design paths do not exist; the same files are at the project root. | -| Decided | Slug policy, hidden-version disclosure, Home counting stories, and review storing source_revision only (no text diff) are decided in the other tables. | +| Home | Recent articles show the default-language title with a language code when not default. States loc.l12.home, loc.l12.homeFr | +| Activity | Events gain the language: "Published French version of …". State loc.l12.activity | +| Article menu | Publish and Delete labels name the language on multilingual sites. State loc.l12.menu | +| API URL copy | Copies the version URL with ?lang=. State loc.l12.apiCopy | +| SEO preview | Shows the localized address and canonical. State loc.l12.seoPreview | +| Media usage | Counts a shared image once; lists versions that use it. State loc.l12.media | +| Site text fallback | A site-level text (for example a site description) is a fallback only for articles in the same language. A French article can use the French site text; an Arabic article with no Arabic site text gets "No fallback" and the tag is left out. State loc.l12.siteFallback | +| Verification limit | Repository files were not accessible. Statements about them are assumptions marked verify in repo. | +| Decided | Slug policy, hidden-version disclosure, Home counting articles, and review storing the source version only (no text diff) are decided in the other tables. | ## Visibility and anti-enumeration policy | Situation | User can read it | User cannot read it | | --- | --- | --- | -| Duplicate translation | "French already exists for this story." Action: Open French version | "French can't be created for this story." No Open action, no title, no hint a version exists | +| Duplicate translation | "French already exists for this article." Action: Open French version | "French can't be created for this article." No Open action, no title, no hint a version exists | | Create conflict response | 409 language_exists with article_id | 409 language_unavailable; identical body for exists-unreadable, disabled and lost race | -| Story delete confirmation | Names readable versions: "Delete English and French?" | Unreadable versions are not named or counted. If any blocks deletion: "You can't delete this story." plus Delete this version | +| Article delete confirmation | Names readable versions: "Delete English and French?" | Unreadable versions are not named or counted. If any blocks deletion: "You can't delete this article." plus Delete this version | | Source unavailable | Deleted: "The source was deleted." | "The source isn't available to you." | | Missing translation tab | Row shown; Add French translation | Same row; a 409 shows the neutral copy | | Counts and summaries | Readable versions only | Never included | @@ -174,30 +190,30 @@ Imported from `source/locale-spec.js`. Read `localization.md` first: its explici | Filters | Status, Author, SEO, Needs review: a story matches if any readable version matches (language mode: the version) | | | | | | Story Author, Updated | Union of readable authors; latest readable update | | | | | | Differing sites | Language filter lists the union; Missing excludes sites that do not enable the language | | | | | -| Dataset (locale-fixtures.js) | Journal: 8 stories, 17 versions, page size 5. Enabled en (default), fr, de, ar; disabled es, it. Versions: en 7 (6 published), fr 4 (2 published, 2 Needs review), de 4 (3 published), ar 1, es 1, it 0. 4 stories have no French version. Needs translation 7, Needs review 2. Every list, tab count, summary and pager is computed from this one fixture. Separate fixtures: Press (3 stories, all translated, nothing-missing state), Atelier (8 languages, long labels), Journal before French (prototype start). | | | | | +| Dataset (locale-fixtures.js) | See the Fixture index. Journal: 8 articles, 17 versions, page size 5 (page 1 shows 5, page 2 shows 3). Enabled en (default), fr, de, ar; disabled es, it. Versions: en 7 (6 published), fr 4 (2 published, 2 Needs review), de 4 (3 published), ar 1, es 1, it 0. 4 articles have no French version. Needs translation 7, Needs review 2. Every list, tab count, unit label and pager is computed from locale-fixtures.js. | | | | | ## Review source and revision semantics | Topic | Decision | | --- | --- | | Record | { article_id, source_article_id, source_revision, translation_revision, reviewed_by, reviewed_at } | -| Confirmation guard | Dialog carries translation_revision. If either revision moved: 409 source_changed or translation_changed; dialog says "This changed since you opened it. Review again." Nothing is marked | +| Confirmation guard | The dialog carries the versions the user saw. If either moved, the dialog says "The English source changed while you were reviewing. Review again." or "French was changed by someone else while you were reviewing. Review again." Nothing is marked | | Change tracked source | Explicit action Change tracked source. Result: Needs review. Earlier reviews are kept as history | | Cycles | Prevented (422 source_cycle). Source must be in the same story | | Access loss vs deletion | Access lost: "The source isn't available to you right now." history kept, Mark reviewed disabled. Deleted: "Source deleted" with Choose a new source | | Source replacement | Pick another readable version of the story; freshness becomes Needs review | -| What changed | Changed-field summary only (Title, Body, Excerpt, SEO) from field-level revision flags. No text diff this release; label "Changed fields" | +| What changed | Changed-field summary only, shown as "Changed fields: Title, Body", with the source version date and the translation version date. No text diff and no revision numbers in the UI | | Concurrent edits | Mark reviewed applies only to the revisions the user saw | ## Title, slug and concurrent edit flows | Case | Behavior | | --- | --- | -| Title editing | Inline input in heading style. States: pristine, editing, invalid ("Enter a title"), saving, failed (value kept), unsaved. Accessible label "Title (French)" | -| Slug | See Slug policy. Edit slug lives in Search and sharing | -| Conflict | "Someone else saved this version." Choices: Keep my changes (nothing sent), Download my version (title and body as text), Save over theirs (sends if_revision with the revision fetched now; a newer save gives a new 409), Reload theirs (confirm: "This discards your unsaved changes.") | +| Title editing | Inline input in heading style. States: pristine, focus, editing, unsaved, invalid ("Enter a title"), too short, too long, saving, failed (value kept). Bounds 3 to 200 characters. Accessible label "Title (French)". Editing the title never changes the slug | +| Slug | See Slug policy. Edit slug lives in Search and sharing and in the overflow menu; its note says "Editing the title never changes the slug." | +| Conflict | "Someone else saved this version at