1778 lines
48 KiB
JavaScript
1778 lines
48 KiB
JavaScript
|
|
window.LOC = {
|
|
"groups": [
|
|
{
|
|
"id": "L1",
|
|
"title": "Site languages",
|
|
"route": "/settings/languages",
|
|
"api": "proposed: GET/PUT /v1/site/{key}/languages",
|
|
"note": "Navigation model: Settings → Sites → [site] → Languages only (no global Languages entry); the site name is in the heading and breadcrumb. Site formatting locale, default content language, article language and social metadata 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. Existing 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. Disabled-language versions count in story-move validation.",
|
|
"states": [
|
|
[
|
|
"Single language",
|
|
"Owner opens Languages on Archive (one language, pt-BR kept)",
|
|
"sl",
|
|
"single"
|
|
],
|
|
[
|
|
"Multilingual",
|
|
"Six languages, two disabled (Spanish, Italian)",
|
|
"sl",
|
|
"multi"
|
|
],
|
|
[
|
|
"Loading",
|
|
"Request in flight",
|
|
"sl",
|
|
"loading"
|
|
],
|
|
[
|
|
"Load failed",
|
|
"Network or server error",
|
|
"sl",
|
|
"error"
|
|
],
|
|
[
|
|
"Add a language",
|
|
"Add language",
|
|
"sl",
|
|
"add"
|
|
],
|
|
[
|
|
"Add failed",
|
|
"Server error, selection kept",
|
|
"sl",
|
|
"saveFail"
|
|
],
|
|
[
|
|
"Duplicate language",
|
|
"Already enabled",
|
|
"sl",
|
|
"adddup"
|
|
],
|
|
[
|
|
"Make default",
|
|
"Row menu",
|
|
"sl",
|
|
"default"
|
|
],
|
|
[
|
|
"Disable with content",
|
|
"Row menu → Disable German",
|
|
"sl",
|
|
"disable"
|
|
],
|
|
[
|
|
"Disable default blocked",
|
|
"Disable English",
|
|
"sl",
|
|
"disableDefault"
|
|
],
|
|
[
|
|
"Disable last blocked",
|
|
"Only one enabled",
|
|
"sl",
|
|
"disableLast"
|
|
],
|
|
[
|
|
"Re-enable",
|
|
"Spanish row → Re-enable",
|
|
"sl",
|
|
"reenable"
|
|
],
|
|
[
|
|
"Remove (no content)",
|
|
"Italian (0 versions)",
|
|
"sl",
|
|
"remove"
|
|
],
|
|
[
|
|
"Remove blocked",
|
|
"Spanish has versions",
|
|
"sl",
|
|
"removeBlocked"
|
|
],
|
|
[
|
|
"Read-only (403)",
|
|
"Non-owner: list stays readable; no Add language, Re-enable or row menus",
|
|
"sl",
|
|
"readonly"
|
|
],
|
|
[
|
|
"Saved",
|
|
"After Add language",
|
|
"sl",
|
|
"saved"
|
|
]
|
|
]
|
|
},
|
|
{
|
|
"id": "L2",
|
|
"title": "First-time setup for existing articles",
|
|
"route": "/settings/sites/:key/languages (banner) · /settings/languages/setup",
|
|
"api": "proposed: GET /v1/language/setup, POST /v1/language/setup { sites:[{ key, lang }] } (lang is the full tag, e.g. pt-BR)",
|
|
"note": "Existing articles have no language. 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), not Portuguese. A site with no locale has no seed and must be chosen. The owner confirms or changes every row before anything applies, including a site with no articles.",
|
|
"states": [
|
|
[
|
|
"Ready to assign",
|
|
"Journal (en), Notes (no locale, must choose), Archive (pt-BR kept)",
|
|
"su",
|
|
"ready"
|
|
],
|
|
[
|
|
"Site with no articles",
|
|
"Gallery has no articles; en-GB kept, confirm and Save",
|
|
"su",
|
|
"empty"
|
|
],
|
|
[
|
|
"Applying",
|
|
"POST in flight",
|
|
"su",
|
|
"applying"
|
|
],
|
|
[
|
|
"Done",
|
|
"16 articles labelled",
|
|
"su",
|
|
"done"
|
|
],
|
|
[
|
|
"Failed",
|
|
"Nothing changed; choices kept",
|
|
"su",
|
|
"fail"
|
|
]
|
|
]
|
|
},
|
|
{
|
|
"id": "L3",
|
|
"title": "New article",
|
|
"route": "/articles (New article modal)",
|
|
"api": "existing POST /v1/article/ + proposed lang, story_id",
|
|
"note": "Single-language sites show no language field. Multilingual sites show Language and a New story / Translation of choice. The Translation path uses the same endpoint and rules as Add translation (POST /v1/article/{id}/translations). The story picker lists only stories the user can read; the chosen version is the source.",
|
|
"states": [
|
|
[
|
|
"Single language",
|
|
"Title and site only",
|
|
"na",
|
|
"single"
|
|
],
|
|
[
|
|
"Multilingual: new story",
|
|
"Language defaults to the site default",
|
|
"na",
|
|
"multi"
|
|
],
|
|
[
|
|
"Translation of a story",
|
|
"Story picker + language",
|
|
"na",
|
|
"translation"
|
|
],
|
|
[
|
|
"Site changed",
|
|
"Selected language not offered by new site",
|
|
"na",
|
|
"sitechange"
|
|
]
|
|
]
|
|
},
|
|
{
|
|
"id": "L4",
|
|
"title": "Add a translation",
|
|
"route": "/article/{id}/edit?dialog=add-translation&lang=fr",
|
|
"api": "proposed: POST /v1/article/{id}/translations",
|
|
"note": "Always creates a draft. Source selection: the user picks which readable version to copy from (default: the default-language version if readable, else the first readable). source_article_id and source_revision are captured server-side 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 source revision is tracked until the author picks a tracked source, so freshness shows No source (not Needs review).",
|
|
"states": [
|
|
[
|
|
"Copy from source",
|
|
"Editor → language menu → Add translation",
|
|
"at",
|
|
"form"
|
|
],
|
|
[
|
|
"Start empty",
|
|
"Title only",
|
|
"at",
|
|
"empty"
|
|
],
|
|
[
|
|
"Creating",
|
|
"Submit in flight",
|
|
"at",
|
|
"saving"
|
|
],
|
|
[
|
|
"Exists, readable",
|
|
"409 language_exists, Open offered",
|
|
"at",
|
|
"dup"
|
|
],
|
|
[
|
|
"Exists, not readable",
|
|
"Neutral 409, no Open",
|
|
"at",
|
|
"dupHidden"
|
|
],
|
|
[
|
|
"Failed",
|
|
"5xx, entries kept",
|
|
"at",
|
|
"fail"
|
|
],
|
|
[
|
|
"Source unavailable",
|
|
"Source deleted or access lost mid-flow",
|
|
"at",
|
|
"gone"
|
|
],
|
|
[
|
|
"Long dialog",
|
|
"Long title, many authors and images; body scrolls",
|
|
"at",
|
|
"long"
|
|
]
|
|
]
|
|
},
|
|
{
|
|
"id": "L5",
|
|
"title": "Editor: language navigation",
|
|
"route": "/article/{id}/edit (one id per version)",
|
|
"api": "proposed: GET /v1/article/{id} → translations[]",
|
|
"note": "The control lists only versions the user can read. Missing languages offer Add translation only if the user may create articles on the site and a readable source exists. Switching opens another article id; the URL is the state. The control is a menu button with a menu (not a listbox), so rows can hold state badges without nesting interactive controls. Permission cases are in the capability matrix.",
|
|
"states": [
|
|
[
|
|
"Default (French draft)",
|
|
"Editable title, language menu, badges",
|
|
"ed",
|
|
"default"
|
|
],
|
|
[
|
|
"Menu: four languages",
|
|
"Open menu",
|
|
"ed",
|
|
"menu"
|
|
],
|
|
[
|
|
"Menu: two languages",
|
|
"Two-language site",
|
|
"ed",
|
|
"two"
|
|
],
|
|
[
|
|
"Menu: many languages",
|
|
"Scrolls inside the viewport, with filter",
|
|
"ed",
|
|
"many"
|
|
],
|
|
[
|
|
"Unsaved changes",
|
|
"Dirty badge; Save enabled",
|
|
"ed",
|
|
"dirty"
|
|
],
|
|
[
|
|
"Unsaved draft: Save and publish",
|
|
"Button reads Save and publish French; Save stays separate",
|
|
"ed",
|
|
"dirtyPublish"
|
|
],
|
|
[
|
|
"Unsaved online version",
|
|
"Save updates the live page; Unpublish stays available",
|
|
"ed",
|
|
"onlineDirty"
|
|
],
|
|
[
|
|
"Save failed",
|
|
"Stays unsaved; Try saving again; nothing published",
|
|
"ed",
|
|
"saveFail"
|
|
],
|
|
[
|
|
"Switch with unsaved changes",
|
|
"Choose English while dirty",
|
|
"ed",
|
|
"unsaved"
|
|
],
|
|
[
|
|
"Saving",
|
|
"Save in flight",
|
|
"ed",
|
|
"saving"
|
|
],
|
|
[
|
|
"Saved",
|
|
"Toast",
|
|
"ed",
|
|
"saved"
|
|
],
|
|
[
|
|
"Loading version",
|
|
"Slow switch",
|
|
"ed",
|
|
"loading"
|
|
],
|
|
[
|
|
"Switch failed",
|
|
"Stays on current version",
|
|
"ed",
|
|
"navFail"
|
|
],
|
|
[
|
|
"Version unavailable",
|
|
"Deleted or no access (neutral)",
|
|
"ed",
|
|
"gone"
|
|
],
|
|
[
|
|
"Title: editing",
|
|
"Focus",
|
|
"ed",
|
|
"focus"
|
|
],
|
|
[
|
|
"Title: invalid",
|
|
"Empty",
|
|
"ed",
|
|
"invalid"
|
|
],
|
|
[
|
|
"Title: saving",
|
|
"",
|
|
"ed",
|
|
"titleSaving"
|
|
],
|
|
[
|
|
"Title: save failed",
|
|
"Text kept, still unsaved, Try again",
|
|
"ed",
|
|
"failed"
|
|
]
|
|
]
|
|
},
|
|
{
|
|
"id": "L5b",
|
|
"title": "Permission states (per version)",
|
|
"route": "/article/{id}/edit",
|
|
"api": "capabilities[] on GET /v1/article/{id}; every PATCH/DELETE rechecked on the server",
|
|
"note": "Each action renders from its own capability. See the capability matrix. Missing-translation rows appear only for users who can create; unreadable siblings are absent with no placeholder.",
|
|
"states": [
|
|
[
|
|
"Read-only version",
|
|
"No Save, Publish, Delete, toolbar",
|
|
"ed",
|
|
"ro"
|
|
],
|
|
[
|
|
"Read-only: menu",
|
|
"No Add translation row",
|
|
"ed",
|
|
"roMenu"
|
|
],
|
|
[
|
|
"Edit, no publish",
|
|
"Save only",
|
|
"ed",
|
|
"p2"
|
|
],
|
|
[
|
|
"Publish, no edit",
|
|
"Publish only; fields read-only",
|
|
"ed",
|
|
"p3"
|
|
],
|
|
[
|
|
"Sibling readable, not editable",
|
|
"German tagged Read only",
|
|
"ed",
|
|
"p4"
|
|
],
|
|
[
|
|
"Sibling not readable",
|
|
"German absent, no count",
|
|
"ed",
|
|
"p5"
|
|
],
|
|
[
|
|
"Can create, not modify all",
|
|
"Story delete and move disabled",
|
|
"ed",
|
|
"p6"
|
|
]
|
|
]
|
|
},
|
|
{
|
|
"id": "L6",
|
|
"title": "Source reference",
|
|
"route": "/article/{id}/edit?ref=:sourceId",
|
|
"api": "proposed: GET /v1/article/{id} (read-only fetch of ref)",
|
|
"note": "Optional. Show source reference is in the editor toolbar menu (desktop) and the overflow sheet (mobile). The temporary reference (?ref=) and the tracked review source (source_article_id) are separate: changing the reference never changes the tracked source; Change tracked source is a separate explicit action in the review banner. Each pane scrolls independently. dir and lang go on the title input and body pane of the translation and the reference, never on the app shell.",
|
|
"states": [
|
|
[
|
|
"Side by side",
|
|
"Show source reference",
|
|
"src",
|
|
"split"
|
|
],
|
|
[
|
|
"Reference hidden",
|
|
"Hide source reference; button returns",
|
|
"src",
|
|
"closed"
|
|
],
|
|
[
|
|
"Reopened",
|
|
"Show source reference again; scroll positions kept",
|
|
"src",
|
|
"reopen"
|
|
],
|
|
[
|
|
"Change reference",
|
|
"Menu; tracked source stays English",
|
|
"src",
|
|
"changeRef"
|
|
],
|
|
[
|
|
"Narrow desktop (1024 px)",
|
|
"True 1024 px frame",
|
|
"src",
|
|
"narrow",
|
|
"narrow"
|
|
],
|
|
[
|
|
"Long content",
|
|
"Both panes scroll independently",
|
|
"src",
|
|
"long"
|
|
],
|
|
[
|
|
"Arabic beside English",
|
|
"dir=rtl on the Arabic title and body only",
|
|
"src",
|
|
"rtl"
|
|
],
|
|
[
|
|
"Mobile: editing",
|
|
"Segment",
|
|
"src",
|
|
"mobileEdit"
|
|
],
|
|
[
|
|
"Mobile: source",
|
|
"Segment",
|
|
"src",
|
|
"mobileSrc"
|
|
],
|
|
[
|
|
"Mobile: closed",
|
|
"Show source reference button",
|
|
"src",
|
|
"mobileClosed"
|
|
],
|
|
[
|
|
"Reference unavailable",
|
|
"Deleted or unreadable",
|
|
"src",
|
|
"unavail"
|
|
]
|
|
]
|
|
},
|
|
{
|
|
"id": "L7",
|
|
"title": "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)",
|
|
"note": "Journal fixture: 8 stories, 17 versions, page size 5. All languages: one row per story (units: stories). Language selected: one row per version in that language (units: versions). tab=missing with a language: one row per story lacking it (units: stories); without a language: stories lacking any enabled language. tab=review: any readable version needs review. Published and Drafts tabs are the existing status filter (online, draft). Mobile: filter sheet (Site, Status, Author, SEO, Language) with Apply and Clear, scrollable tab strip with edge fade and chevrons, same pager and page size as desktop. All state is in the URL.",
|
|
"states": [
|
|
[
|
|
"All languages",
|
|
"Stories",
|
|
"li",
|
|
"all"
|
|
],
|
|
[
|
|
"French selected",
|
|
"Versions",
|
|
"li",
|
|
"fr"
|
|
],
|
|
[
|
|
"Not in French",
|
|
"Missing-translation discovery",
|
|
"li",
|
|
"frMissing"
|
|
],
|
|
[
|
|
"Nothing missing",
|
|
"Separate fixture: Press (3 stories, all translated). Empty; no summary, no pager",
|
|
"li",
|
|
"emptyMissing"
|
|
],
|
|
[
|
|
"No results",
|
|
"Search \"zinc\"; every count 0",
|
|
"li",
|
|
"noResults"
|
|
],
|
|
[
|
|
"Loading",
|
|
"",
|
|
"li",
|
|
"frLoading"
|
|
],
|
|
[
|
|
"Failed",
|
|
"Filters kept",
|
|
"li",
|
|
"listFail"
|
|
],
|
|
[
|
|
"Limited access",
|
|
"No dashed missing chips",
|
|
"li",
|
|
"limited"
|
|
],
|
|
[
|
|
"Mobile filter sheet",
|
|
"Journal, French, Draft: 2 results",
|
|
"li",
|
|
"filters"
|
|
],
|
|
[
|
|
"Tabs scrolled",
|
|
"Not in French reachable; fade and chevrons",
|
|
"li",
|
|
"tabsScroll"
|
|
],
|
|
[
|
|
"Long labels, many languages, filters",
|
|
"Separate fixture: Atelier (8 languages), active filter chips",
|
|
"li",
|
|
"long"
|
|
]
|
|
]
|
|
},
|
|
{
|
|
"id": "L8",
|
|
"title": "Publishing and deletion",
|
|
"route": "same as editor",
|
|
"api": "existing PATCH /v1/article/{id}/publish and /unpublish and DELETE per article id",
|
|
"note": "Every action names its language. There is no bulk publish. Changing the source does not unpublish translations. While a version has unsaved edits the primary action reads Save and publish and saves first. If the save fails nothing is published and the edits stay unsaved. Unpublish never needs a save. There are no unpublished revisions: saving an online version updates the live page immediately, and the editor says so.",
|
|
"states": [
|
|
[
|
|
"Publish French",
|
|
"Confirm, English untouched",
|
|
"pd",
|
|
"publish"
|
|
],
|
|
[
|
|
"Save and publish French",
|
|
"Unsaved edits: one action saves first, then publishes",
|
|
"pd",
|
|
"saveAndPublish"
|
|
],
|
|
[
|
|
"Save failed, nothing published",
|
|
"Toast; edits kept and retryable",
|
|
"pd",
|
|
"saveFailPublish"
|
|
],
|
|
[
|
|
"Unpublish French",
|
|
"Confirm",
|
|
"pd",
|
|
"unpublish"
|
|
],
|
|
[
|
|
"Delete French version",
|
|
"Confirm",
|
|
"pd",
|
|
"delete"
|
|
],
|
|
[
|
|
"Delete a source version",
|
|
"Warns about review reference",
|
|
"pd",
|
|
"deleteSource"
|
|
],
|
|
[
|
|
"Delete last version",
|
|
"Deletes the story",
|
|
"pd",
|
|
"deleteLast"
|
|
],
|
|
[
|
|
"Delete story blocked",
|
|
"Neutral, no version named",
|
|
"pd",
|
|
"deleteStory"
|
|
],
|
|
[
|
|
"Published",
|
|
"Toast",
|
|
"pd",
|
|
"done"
|
|
],
|
|
[
|
|
"Publish failed",
|
|
"Toast",
|
|
"pd",
|
|
"fail"
|
|
]
|
|
]
|
|
},
|
|
{
|
|
"id": "L9",
|
|
"title": "Freshness and review",
|
|
"route": "editor banner · list badge",
|
|
"api": "proposed: POST /v1/article/{id}/review { source_article_id, source_revision, translation_revision }",
|
|
"note": "Publication (solid soft badge) and freshness (outlined badge with refresh icon) are separate. Labels: No version, Draft, Needs review, Reviewed. A copied draft starts as Needs review.",
|
|
"states": [
|
|
[
|
|
"Source updated",
|
|
"English changed after review",
|
|
"fr",
|
|
"warn"
|
|
],
|
|
[
|
|
"Published, needs review",
|
|
"",
|
|
"fr",
|
|
"pub"
|
|
],
|
|
[
|
|
"Mark reviewed",
|
|
"Confirm with revisions",
|
|
"fr",
|
|
"review"
|
|
],
|
|
[
|
|
"Source changed again",
|
|
"Revision 9 while reviewing 8",
|
|
"fr",
|
|
"changed"
|
|
],
|
|
[
|
|
"Translation changed",
|
|
"Saved by someone else while confirming",
|
|
"fr",
|
|
"changedTr"
|
|
],
|
|
[
|
|
"Choose tracked source",
|
|
"Cycle prevented",
|
|
"fr",
|
|
"replace"
|
|
],
|
|
[
|
|
"Source unavailable (access)",
|
|
"History kept",
|
|
"fr",
|
|
"inaccessible"
|
|
],
|
|
[
|
|
"Source deleted",
|
|
"Replace",
|
|
"fr",
|
|
"gone"
|
|
],
|
|
[
|
|
"Reviewed",
|
|
"Toast",
|
|
"fr",
|
|
"done"
|
|
],
|
|
[
|
|
"Review failed",
|
|
"Toast",
|
|
"fr",
|
|
"fail"
|
|
]
|
|
]
|
|
},
|
|
{
|
|
"id": "L10",
|
|
"title": "Metadata and public API",
|
|
"route": "Search and sharing panel · /integration",
|
|
"api": "existing slug, SEO fields; proposed story_id, alternates[], lang; exact lookup by site + lang + slug; resolution by story_id",
|
|
"note": "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. Public website URLs show only when the site has a public URL pattern; otherwise the panel says Public URL not configured. Canonical is the localized page itself unless an explicit canonical override is set.",
|
|
"states": [
|
|
[
|
|
"Search and sharing: French",
|
|
"Localized slug, canonical, alternates",
|
|
"seo",
|
|
""
|
|
],
|
|
[
|
|
"Public URL not configured",
|
|
"No pattern, no fabricated URL",
|
|
"seo",
|
|
"unset"
|
|
],
|
|
[
|
|
"Social locale not set",
|
|
"fr has no region and none is configured: og:locale omitted",
|
|
"seo",
|
|
"noSocial"
|
|
],
|
|
[
|
|
"Integration: languages",
|
|
"Proposed requests",
|
|
"api",
|
|
""
|
|
]
|
|
]
|
|
},
|
|
{
|
|
"id": "L11",
|
|
"title": "Permissions, conflicts, site moves",
|
|
"route": "various",
|
|
"api": "existing 401/403/404/409 patterns",
|
|
"note": "Story membership grants nothing: every action is checked per version on the server. Stories move between sites as a whole, only when the destination enables every language they contain (disabled ones included) and the user can edit all versions. Moves and story deletion are atomic and revalidate permissions and conflicts in the transaction.",
|
|
"states": [
|
|
[
|
|
"Session expired",
|
|
"Draft kept locally",
|
|
"pm",
|
|
"session"
|
|
],
|
|
[
|
|
"Concurrent update",
|
|
"Four choices, local work kept",
|
|
"pm",
|
|
"conflict"
|
|
],
|
|
[
|
|
"Move: language missing",
|
|
"",
|
|
"pm",
|
|
"move"
|
|
],
|
|
[
|
|
"Move: address conflict",
|
|
"",
|
|
"pm",
|
|
"moveSlug"
|
|
],
|
|
[
|
|
"Move: no permission",
|
|
"Neutral",
|
|
"pm",
|
|
"movePerm"
|
|
]
|
|
]
|
|
}
|
|
],
|
|
"tables": [
|
|
{
|
|
"title": "Decisions",
|
|
"cols": [
|
|
"Question",
|
|
"Chosen behavior",
|
|
"Rationale",
|
|
"Existing behavior affected",
|
|
"Backend dependency",
|
|
"Release"
|
|
],
|
|
"w": "1fr 1.6fr 1.3fr 1fr 1fr 70px",
|
|
"rows": [
|
|
[
|
|
"Language model",
|
|
"One story group, one article id per language, one version per locale",
|
|
"Reuses the article editor, permissions and publish flow",
|
|
"None for single-language sites",
|
|
"group_id, lang on articles; unique (group_id, lang)",
|
|
"Initial"
|
|
],
|
|
[
|
|
"Default vs editing vs source",
|
|
"Three separate roles. Default is a site setting; editing language is the open article; source is chosen per translation",
|
|
"Default is not always the best source",
|
|
"None",
|
|
"source_article_id per version",
|
|
"Initial"
|
|
],
|
|
[
|
|
"Locale scope",
|
|
"Per site, list of enabled languages plus one default",
|
|
"Matches site-level settings",
|
|
"Settings → Sites gains Languages",
|
|
"site_languages table",
|
|
"Initial"
|
|
],
|
|
[
|
|
"Site locale field",
|
|
"Four distinct values: formatting locale (existing Site.Locale, still used for dates and numbers), default content language (new, seeded from the full Site.Locale and confirmed by the owner), article language (per version), social metadata locale (explicit or derived, never guessed)",
|
|
"Avoids silent reinterpretation",
|
|
"ResolveArticleSEO reads Site.Locale for social metadata (verify in repo); for localized articles it uses the article language",
|
|
"None",
|
|
"Initial"
|
|
],
|
|
[
|
|
"Disable vs remove",
|
|
"Disable keeps content and published versions. Remove only at zero versions",
|
|
"No configuration action deletes or unpublishes",
|
|
"None",
|
|
"Count per language; 409 language_in_use",
|
|
"Initial"
|
|
],
|
|
[
|
|
"Existing content",
|
|
"Setup assigns a language per site, in one transaction; each article becomes its own story",
|
|
"Metadata only; no guessed grouping",
|
|
"Articles gain lang",
|
|
"Bulk assign endpoint, atomic",
|
|
"Initial"
|
|
],
|
|
[
|
|
"Titles",
|
|
"Editable after creation. A title change never alters an existing slug. Slug changes only through explicit Edit slug; no redirects are promised (none in the backend)",
|
|
"Translated titles must be revisable; URLs stay stable. Same rule on single-language sites",
|
|
"CHANGE: titles were immutable. Slug policy applies everywhere",
|
|
"Allow title in PUT; slug rules",
|
|
"Initial"
|
|
],
|
|
[
|
|
"Slug uniqueness",
|
|
"Unique per (site, lang)",
|
|
"Same slug in two languages is normal",
|
|
"Today unique per site",
|
|
"Index change",
|
|
"Initial"
|
|
],
|
|
[
|
|
"Language control",
|
|
"Dropdown beside the title; rows show version state; interface language unchanged",
|
|
"Compact for 2 to 20 languages",
|
|
"Editor heading",
|
|
"translations[] on GET article",
|
|
"Initial"
|
|
],
|
|
[
|
|
"Switching",
|
|
"Opens another article id. Dirty form asks Keep editing, Discard, Save and switch",
|
|
"URL is the state; no lost work",
|
|
"None",
|
|
"None",
|
|
"Initial"
|
|
],
|
|
[
|
|
"Add translation",
|
|
"Draft only; copy or start empty; images/authors reused; copied text flagged Needs review",
|
|
"Copying is a start, not completion",
|
|
"None",
|
|
"POST translations; 409 language_exists",
|
|
"Initial"
|
|
],
|
|
[
|
|
"Source reference",
|
|
"Optional read-only pane; segment on mobile; no sync",
|
|
"Keeps the single editor default",
|
|
"None",
|
|
"Read access to source",
|
|
"Initial"
|
|
],
|
|
[
|
|
"Direction",
|
|
"dir on editor and reference panes only",
|
|
"Shell stays LTR",
|
|
"None",
|
|
"Per-language dir from locale",
|
|
"Initial"
|
|
],
|
|
[
|
|
"List: all languages",
|
|
"One row per story; title from default version, else first accessible; coverage chips",
|
|
"Avoids duplicates",
|
|
"Articles list",
|
|
"Grouped query, access-filtered",
|
|
"Initial"
|
|
],
|
|
[
|
|
"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"
|
|
],
|
|
[
|
|
"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"
|
|
],
|
|
[
|
|
"Hidden versions",
|
|
"Titles, counts, coverage, language summaries, previews, errors and deletion confirmations name only versions the user can read",
|
|
"No leakage",
|
|
"List counts, delete dialog",
|
|
"Filter at query level",
|
|
"Initial"
|
|
],
|
|
[
|
|
"Machine translation",
|
|
"Not included",
|
|
"Out of scope",
|
|
"None",
|
|
"None",
|
|
"Later"
|
|
],
|
|
[
|
|
"Cross-site stories",
|
|
"Not included",
|
|
"Out of scope",
|
|
"None",
|
|
"None",
|
|
"Later"
|
|
],
|
|
[
|
|
"Bulk publish / review",
|
|
"Not included",
|
|
"No clear need",
|
|
"None",
|
|
"None",
|
|
"Later"
|
|
],
|
|
[
|
|
"Slug policy",
|
|
"Slug generated once at creation. Changed only by Edit slug, before or after publishing. Published change warns: \"Links to the old address will stop working. RenewCMS does not create redirects.\"",
|
|
"One policy, no redirect promise",
|
|
"Slug field",
|
|
"Unique (site, lang)",
|
|
"Initial"
|
|
],
|
|
[
|
|
"Permission model",
|
|
"Capabilities per version: read, edit, publish, delete, create-translation, review. UI renders from capabilities[] on the article response, not from role names",
|
|
"Matches role/scope permissions",
|
|
"Editor, list",
|
|
"capabilities[]; server enforces every call",
|
|
"Initial"
|
|
],
|
|
[
|
|
"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"
|
|
]
|
|
]
|
|
},
|
|
{
|
|
"title": "Capability matrix (per version, enforced server-side)",
|
|
"cols": [
|
|
"Case",
|
|
"Read",
|
|
"Edit",
|
|
"Publish",
|
|
"Delete",
|
|
"Create translation",
|
|
"Review",
|
|
"UI result"
|
|
],
|
|
"w": "1.3fr .5fr .5fr .6fr .6fr .8fr .6fr 2.2fr",
|
|
"rows": [
|
|
[
|
|
"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"
|
|
],
|
|
[
|
|
"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"
|
|
],
|
|
[
|
|
"Owner / Admin",
|
|
"Yes",
|
|
"Yes",
|
|
"Yes",
|
|
"Yes",
|
|
"Yes",
|
|
"Yes",
|
|
"All readable versions; not inferred from role label alone"
|
|
]
|
|
]
|
|
},
|
|
{
|
|
"title": "Field ownership, copy and fallback",
|
|
"cols": [
|
|
"Field",
|
|
"Own per version",
|
|
"Copied on \"Copy from source\"",
|
|
"Fallback if empty"
|
|
],
|
|
"w": "1.4fr 1fr 1.4fr 1.6fr",
|
|
"rows": [
|
|
[
|
|
"Title",
|
|
"Yes",
|
|
"Entered by the editor",
|
|
"None (required)"
|
|
],
|
|
[
|
|
"Slug",
|
|
"Yes",
|
|
"Generated from the new title",
|
|
"None"
|
|
],
|
|
[
|
|
"Body",
|
|
"Yes",
|
|
"Yes, flagged Needs review",
|
|
"None"
|
|
],
|
|
[
|
|
"Excerpt",
|
|
"Yes",
|
|
"Yes",
|
|
"First words of body"
|
|
],
|
|
[
|
|
"SEO title and description",
|
|
"Yes",
|
|
"Yes",
|
|
"Title / excerpt, then site defaults"
|
|
],
|
|
[
|
|
"Social text",
|
|
"Yes",
|
|
"Yes",
|
|
"SEO text"
|
|
],
|
|
[
|
|
"Image alt text",
|
|
"Yes",
|
|
"Yes",
|
|
"None"
|
|
],
|
|
[
|
|
"Cover and inline images",
|
|
"Reference, files shared",
|
|
"Yes, no duplicate files",
|
|
"None"
|
|
],
|
|
[
|
|
"Authors",
|
|
"Yes",
|
|
"Yes, editable",
|
|
"None"
|
|
],
|
|
[
|
|
"Site",
|
|
"Shared by the story",
|
|
"Same",
|
|
"None"
|
|
],
|
|
[
|
|
"Publication and timestamps",
|
|
"Yes",
|
|
"No",
|
|
"None"
|
|
]
|
|
]
|
|
},
|
|
{
|
|
"title": "API: existing vs proposed",
|
|
"cols": [
|
|
"Capability",
|
|
"Status",
|
|
"Request",
|
|
"Errors"
|
|
],
|
|
"w": "1fr .8fr 2fr 1.6fr",
|
|
"rows": [
|
|
[
|
|
"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"
|
|
],
|
|
[
|
|
"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"
|
|
]
|
|
]
|
|
},
|
|
{
|
|
"title": "Accessibility and interaction",
|
|
"cols": [
|
|
"Control",
|
|
"Accessible name and keyboard",
|
|
"Focus",
|
|
"Announcement"
|
|
],
|
|
"w": "1fr 2fr 1.2fr 1.4fr",
|
|
"rows": [
|
|
[
|
|
"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\""
|
|
],
|
|
[
|
|
"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",
|
|
"",
|
|
""
|
|
]
|
|
]
|
|
},
|
|
{
|
|
"title": "Acceptance scenarios",
|
|
"cols": [
|
|
"#",
|
|
"Given / When / Then"
|
|
],
|
|
"w": "40px 1fr",
|
|
"rows": [
|
|
[
|
|
"1",
|
|
"Given a single-language site, when creating an article, then no language control appears anywhere."
|
|
],
|
|
[
|
|
"2",
|
|
"Given French is enabled, when an editor adds a French translation with \"Copy from English\", then a draft is created, the editor opens it, and the badge reads Draft and Needs review."
|
|
],
|
|
[
|
|
"3",
|
|
"When French is published, then English publication is unchanged."
|
|
],
|
|
[
|
|
"4",
|
|
"When English changes title or body after review, then French shows Needs review; changing authors does not."
|
|
],
|
|
[
|
|
"5",
|
|
"Given dirty French, when choosing English, then a three-choice dialog appears and no work is lost."
|
|
],
|
|
[
|
|
"6",
|
|
"Given a writer who cannot see the German draft, then German appears nowhere in lists, counts, menus or alternates."
|
|
],
|
|
[
|
|
"7",
|
|
"When disabling German with 12 versions, then all 12 stay and 3 published stay online."
|
|
],
|
|
[
|
|
"8",
|
|
"When two editors create French at once, then the second sees \"French is no longer available\" with entries kept."
|
|
],
|
|
[
|
|
"9",
|
|
"When moving a story to a site missing a language, then the move is blocked and nothing splits."
|
|
],
|
|
[
|
|
"10",
|
|
"When the interface language is Arabic or French, the editing language indicator still shows the article language."
|
|
],
|
|
[
|
|
"11",
|
|
"Given French has unsaved edits, when the user chooses Save and publish French and the save fails, then nothing is published and the edits stay unsaved with Try saving again."
|
|
],
|
|
[
|
|
"12",
|
|
"Given French is missing and unpublished, when a client resolves the story with lang=fr and fallback=default and the default is published, then the response is the default language with requested_lang fr and fallback true; with the default also unpublished it is 404 language_not_available."
|
|
],
|
|
[
|
|
"13",
|
|
"Given a site locale pt-BR, when setup seeds the default language, then it reads Portuguese (Brazil) pt-BR and the owner confirms it before anything applies."
|
|
]
|
|
]
|
|
},
|
|
{
|
|
"title": "Existing surfaces audit and verification",
|
|
"cols": [
|
|
"Item",
|
|
"Note"
|
|
],
|
|
"w": "1fr 3fr",
|
|
"rows": [
|
|
[
|
|
"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."
|
|
]
|
|
]
|
|
},
|
|
{
|
|
"title": "Visibility and anti-enumeration policy",
|
|
"cols": [
|
|
"Situation",
|
|
"User can read it",
|
|
"User cannot read it"
|
|
],
|
|
"w": "1.2fr 1.6fr 1.6fr",
|
|
"rows": [
|
|
[
|
|
"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"
|
|
],
|
|
[
|
|
"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"
|
|
],
|
|
[
|
|
"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"
|
|
]
|
|
]
|
|
},
|
|
{
|
|
"title": "Public lookup and URLs",
|
|
"cols": [
|
|
"Topic",
|
|
"Decision"
|
|
],
|
|
"w": "1fr 3fr",
|
|
"rows": [
|
|
[
|
|
"CMS API vs website",
|
|
"CMS API URLs (/v1/...) are never shown as website URLs. Website URLs come from a proposed per-site public_url_pattern (owner), e.g. https://example.com/{lang}/{slug}. Existing convention unverified."
|
|
],
|
|
[
|
|
"Pattern not configured",
|
|
"\"Public URL not configured\" with a link to site settings. No URL is fabricated. API links by article ID remain."
|
|
],
|
|
[
|
|
"Exact lookup",
|
|
"GET /v1/public/site/{key}/lang/{lang}/article/{slug}. Matches site + language + slug only (slug is unique per site and language). Missing or unpublished: 404 article_not_found. It never falls back and never tries another language's slug"
|
|
],
|
|
[
|
|
"Resolution by story",
|
|
"GET /v1/public/site/{key}/story/{story_id}?lang=&fallback=default. story_id is the stable story identifier, returned on every public response and in alternates[]. No lang means the site default language. Fallback is decided only by story_id"
|
|
],
|
|
[
|
|
"Example: requested version published",
|
|
"story st_01, lang=fr, French published: 200, lang fr, fallback false"
|
|
],
|
|
[
|
|
"Example: requested missing or unpublished",
|
|
"lang=fr, no published French. Without fallback: 404 language_not_available. With fallback=default and a published default (en): 200, lang en, requested_lang fr, fallback true"
|
|
],
|
|
[
|
|
"Example: default also missing or unpublished",
|
|
"lang=fr, fallback=default, French and the default both unavailable: 404 language_not_available. No third language is tried"
|
|
],
|
|
[
|
|
"Example: default requested but unpublished",
|
|
"lang=en, English unpublished, fallback=default: 404 language_not_available"
|
|
],
|
|
[
|
|
"Compatibility",
|
|
"Article-ID endpoints (GET /v1/article/{id}) and existing public URLs keep working. Setup labels existing articles with the site default language"
|
|
],
|
|
[
|
|
"Response identity",
|
|
"Always lang (actual language). alternates[] lists only published, enabled-language versions"
|
|
],
|
|
[
|
|
"Canonical",
|
|
"The localized page itself, unless an explicit canonical override is set"
|
|
],
|
|
[
|
|
"Default-language change",
|
|
"Does not change what existing URLs return; absent or unpublished default gives 404 unless fallback is requested"
|
|
]
|
|
]
|
|
},
|
|
{
|
|
"title": "Locale semantics",
|
|
"cols": [
|
|
"Value",
|
|
"Meaning",
|
|
"Used for"
|
|
],
|
|
"w": "1fr 2fr 1.6fr",
|
|
"rows": [
|
|
[
|
|
"Site formatting locale",
|
|
"Existing Site.Locale",
|
|
"SPA dates and numbers; kept"
|
|
],
|
|
[
|
|
"Default content language",
|
|
"New per-site setting. Setup seeds it from the full Site.Locale when a supported language entry matches (en-GB, pt-BR). If only the language part is supported it seeds that and says so. No locale: the owner must choose. The owner confirms or changes it explicitly; nothing applies unconfirmed",
|
|
"Fallback, new-article default, setup"
|
|
],
|
|
[
|
|
"Article language",
|
|
"BCP 47 tag per version (en, fr, pt-BR, ar)",
|
|
"html lang, API lang, hreflang"
|
|
],
|
|
[
|
|
"Social metadata locale",
|
|
"Resolved in order: 1) explicit social locale on the site language (set by an owner, e.g. fr: fr_FR); 2) the language tag already has a region (pt-BR gives pt_BR); 3) Site.Locale has the same language part (site en-GB and article en give en_GB); 4) otherwise none: og:locale and og:locale:alternate are omitted for that version and the editor shows Not set. No region is invented for a bare language. og:locale:alternate lists other published versions whose locale resolves by the same rules; the rest are left out",
|
|
"og:locale, og:locale:alternate"
|
|
],
|
|
[
|
|
"SEO fallbacks",
|
|
"Empty French SEO fields fall back to French title and excerpt. English site defaults are never applied silently to a non-default language: editor shows \"Empty. English site default not used.\" and the tag is omitted",
|
|
"Editor labels: \"Fallback: French title\", \"No fallback\""
|
|
]
|
|
]
|
|
},
|
|
{
|
|
"title": "Article list modes and sample data",
|
|
"cols": [
|
|
"Mode",
|
|
"Units and count",
|
|
"Representative title",
|
|
"Row opens",
|
|
"Search",
|
|
"Sort"
|
|
],
|
|
"w": "1fr 1.2fr 1.4fr 1fr 1.4fr 1fr",
|
|
"rows": [
|
|
[
|
|
"All languages",
|
|
"Stories",
|
|
"Default-language version if readable, else first readable by language code",
|
|
"Representative version",
|
|
"Any readable title",
|
|
"Story updated: latest readable version"
|
|
],
|
|
[
|
|
"Language selected",
|
|
"Versions in that language",
|
|
"That version",
|
|
"That version",
|
|
"That language only",
|
|
"Version updated"
|
|
],
|
|
[
|
|
"Missing language",
|
|
"Stories lacking it or unreadable in it",
|
|
"As All languages",
|
|
"Representative version",
|
|
"Titles in any readable language",
|
|
"Story updated"
|
|
],
|
|
[
|
|
"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).",
|
|
"",
|
|
"",
|
|
"",
|
|
""
|
|
]
|
|
]
|
|
},
|
|
{
|
|
"title": "Review source and revision semantics",
|
|
"cols": [
|
|
"Topic",
|
|
"Decision"
|
|
],
|
|
"w": "1fr 3fr",
|
|
"rows": [
|
|
[
|
|
"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"
|
|
],
|
|
[
|
|
"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\""
|
|
],
|
|
[
|
|
"Concurrent edits",
|
|
"Mark reviewed applies only to the revisions the user saw"
|
|
]
|
|
]
|
|
},
|
|
{
|
|
"title": "Title, slug and concurrent edit flows",
|
|
"cols": [
|
|
"Case",
|
|
"Behavior"
|
|
],
|
|
"w": "1fr 3fr",
|
|
"rows": [
|
|
[
|
|
"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.\")"
|
|
],
|
|
[
|
|
"Overwrite scope",
|
|
"\"Replaces the title, body and search fields of this version only. Other languages are untouched.\""
|
|
],
|
|
[
|
|
"Session expiry",
|
|
"Draft kept locally per article id (title, body, search fields, base revision) until saved or discarded. After login: \"Restored unsaved changes from <time>.\""
|
|
]
|
|
]
|
|
},
|
|
{
|
|
"title": "Language configuration lifecycle",
|
|
"cols": [
|
|
"Action",
|
|
"Rule"
|
|
],
|
|
"w": "1fr 3fr",
|
|
"rows": [
|
|
[
|
|
"Disable default",
|
|
"Blocked: \"Choose another default first.\""
|
|
],
|
|
[
|
|
"Disable last enabled",
|
|
"Blocked: \"At least one language must stay enabled.\""
|
|
],
|
|
[
|
|
"Disable with content",
|
|
"Allowed; versions remain; published stay online; publishing blocked for drafts"
|
|
],
|
|
[
|
|
"Re-enable",
|
|
"Row action Re-enable; no data change"
|
|
],
|
|
[
|
|
"Remove",
|
|
"Only at zero versions; never the default"
|
|
],
|
|
[
|
|
"Save error",
|
|
"Dialog keeps language, default and toggle values; error above the footer"
|
|
],
|
|
[
|
|
"Story moves",
|
|
"Disabled-language versions count toward destination requirements"
|
|
]
|
|
]
|
|
},
|
|
{
|
|
"title": "Mobile list and prototype scope",
|
|
"cols": [
|
|
"Item",
|
|
"Spec"
|
|
],
|
|
"w": "1fr 3fr",
|
|
"rows": [
|
|
[
|
|
"Filter sheet",
|
|
"Site, Status, Author, SEO, Language; Apply, Clear all; chips with \"Remove filter Status: Draft\""
|
|
],
|
|
[
|
|
"Tabs",
|
|
"Scrolls with edge fade and buttons \"Scroll tabs left/right\"; Not in French always reachable"
|
|
],
|
|
[
|
|
"Pager",
|
|
"Same pager and page size as desktop; URL keeps page and size"
|
|
],
|
|
[
|
|
"Prototype flows",
|
|
"Wired in RenewCMS Prototype, flow Localization: F1 enable French, F2 create translation, F3 edit beside English, F4 save and publish French, F5 edit English, F6 freshness warning, F7 review, F8 missing tab, F9 switch with unsaved, F10 delete one version, F11 restricted user. Starts from the Journal-before-French fixture"
|
|
]
|
|
]
|
|
}
|
|
]
|
|
}
|