358 lines
35 KiB
Markdown
358 lines
35 KiB
Markdown
# Localization board tables
|
|
|
|
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
|
|
|
|
| Question | Chosen behavior | Rationale | Existing behavior affected | Backend dependency | Release |
|
|
| --- | --- | --- | --- | --- | --- |
|
|
| 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. 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 |
|
|
| 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 |
|
|
| Header priority | Left to right: language dropdown, publication badge, status badge (unsaved, saving, saved, failed), freshness badge, Read only tag, title input (fills the remaining width, same row on desktop), 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; 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 | 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
|
|
|
|
| Field | Own per version | Copied on "Copy from source" | Fallback if empty |
|
|
| --- | --- | --- | --- |
|
|
| 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 |
|
|
|
|
## API: existing vs proposed
|
|
|
|
| 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 |
|
|
| 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 · 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 the list on open; back to the control on close | Polite: "Editing French version" |
|
|
| Title input | Accessible name "Title (French)", no visible label (the language dropdown beside it names the version); 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". 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
|
|
|
|
| # | Given / When / Then |
|
|
| --- | --- |
|
|
| 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. |
|
|
|
|
## Existing surfaces audit and verification
|
|
|
|
| Item | Note |
|
|
| --- | --- |
|
|
| 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 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 |
|
|
| 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 |
|
|
|
|
## Public lookup and URLs
|
|
|
|
| Topic | Decision |
|
|
| --- | --- |
|
|
| 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 |
|
|
|
|
## Locale semantics
|
|
|
|
| Value | Meaning | Used for |
|
|
| --- | --- | --- |
|
|
| 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" |
|
|
|
|
## Article list modes and sample data
|
|
|
|
| Mode | Units and count | Representative title | Row opens | Search | Sort |
|
|
| --- | --- | --- | --- | --- | --- |
|
|
| 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) | 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 | 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, 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 underline input in heading style, on the same row as the language, badges and actions on desktop (no box, no visible label). 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 <time>." Choices: Keep my changes (filled; nothing sent), Download my version, Reload theirs (confirm: "This discards your unsaved changes."), Save over theirs (sends the version fetched now; a newer save gives a new conflict) |
|
|
| Overwrite scope | "Replaces the title, body and search fields of this version only. Other languages are untouched." |
|
|
| Session expiry | Draft kept locally per article (title, body, search fields) until saved or discarded. After login: "Restored unsaved changes from <time>." |
|
|
|
|
## Language configuration lifecycle
|
|
|
|
| Action | Rule |
|
|
| --- | --- |
|
|
| 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 |
|
|
|
|
## Mobile list and prototype scope
|
|
|
|
| Item | Spec |
|
|
| --- | --- |
|
|
| Filter sheet | Site, Language, Status, Author, SEO; Clear all and "Show N results"; active filters show as chips with "Remove filter Status: Draft" and Clear all |
|
|
| Tabs | Scrolls with edge fade and buttons "Scroll tabs left/right"; Not in French is always reachable |
|
|
| Pager | Same page size as desktop (5); "Page 1 of 2"; the URL keeps page and size |
|
|
| Row actions | Identical capability on every width: see Row actions. Mobile uses the row menu button (44 px), desktop shows the same button (28 px) |
|
|
| Prototype flows | Not part of this pass. The LocaleLive prototype was not touched |
|
|
|
|
## Editor header priority
|
|
|
|
| Order | Item | 1440 | 1024 | 390 |
|
|
| --- | --- | --- | --- | --- |
|
|
| 1 | Title input (underline, heading style; own row on mobile, shares the header row on desktop) | Full width | Full width | Full width |
|
|
| 2 | Language dropdown | Visible | Visible | Visible, 44 px |
|
|
| 3 | Publication badge | Visible | Visible | Visible |
|
|
| 4 | Primary action (the only filled button) | Rightmost before More actions | Same | Fixed bottom bar, fills the width left by Save and More |
|
|
| 5 | Status badge (Unsaved changes, Saving, Saved, Save failed) | Visible | Visible | Visible |
|
|
| 6 | Save (secondary) | Visible | Visible | Bottom bar, next to the primary |
|
|
| 7 | Freshness badge | Icon and word | Collapses first: icon only, name in aria-label | Wraps under the language row |
|
|
| 8 | Read only tag | Visible | Visible | Visible |
|
|
| 9 | Reference button | Icon and label | Collapses second: icon only | More actions sheet |
|
|
| 10 | Unpublish (secondary) | Visible | Collapses third: More actions menu | More actions sheet |
|
|
| 11 | More actions | Edit slug, Move article, Delete versions | Same plus Unpublish | Same plus reference and Unpublish |
|
|
|
|
## Action matrix
|
|
|
|
| State | Permissions | Primary (filled) | Secondary | Notes |
|
|
| --- | --- | --- | --- | --- |
|
|
| Clean draft | edit + publish | Publish French | Save (off) | loc.l5.default |
|
|
| Clean draft | edit only | None | Save (off) | Hint: Publishing needs the publish permission |
|
|
| Dirty draft | edit only | Save | None | loc.l5.dirty; "Saving keeps it as a draft." |
|
|
| Dirty draft | edit + publish | Save and publish French | Save | loc.l5.dirtyPublish |
|
|
| Clean online | edit + publish | None | Unpublish French, Save (off) | loc.l5.cleanOnline; "This version is online. Saving updates the live page right away." |
|
|
| Dirty online | edit + publish | Save | Unpublish French | loc.l5.onlineDirty |
|
|
| Saving | any | Saving… (busy) | None | loc.l5.saving |
|
|
| Saved | any | Publish French if allowed | Save (off) | loc.l5.saved |
|
|
| Save failed | edit | Try saving again | None | loc.l5.saveFail; nothing published |
|
|
| Publish failed after save | edit + publish | Retry publish | Save (off) | loc.l5.pubFail; "Saved as a draft. Publishing failed. Try publishing again." |
|
|
| Unpublish with edits | publish | Unchanged | Unpublish French | loc.l8.unpublishDirty; saved version goes offline, local edits stay, nothing is saved |
|
|
| Read-only | read | None | None | Reference button remains |
|
|
| Publish without edit | publish | Publish French | None | Fields read-only |
|
|
|
|
## Title field states
|
|
|
|
| State | Look | Message | Save |
|
|
| --- | --- | --- | --- |
|
|
| Pristine | Heading-size text, neutral border | None | Off |
|
|
| Focus | Primary 2px underline | "Changing the title doesn't change the address." | Off |
|
|
| Editing | Primary border, counter n / 200 | Same note | On |
|
|
| Unsaved | Neutral border, Unsaved changes badge | Same note | On |
|
|
| Invalid | Error border and icon | "Enter a title" | Blocked |
|
|
| Too short | Error border, counter | "Use at least 3 characters" | Blocked |
|
|
| Too long | Error border, counter 203 / 200 | "Use 200 characters or fewer" | Blocked |
|
|
| Saving | Spinner | "Saving title…" | Busy |
|
|
| Save failed | Error border and icon | "Couldn't save the title. Your text is kept." Try again | On |
|
|
|
|
## Row actions (same on every width)
|
|
|
|
| Action | Shown when | Desktop | Mobile |
|
|
| --- | --- | --- | --- |
|
|
| Open <language> | Always, for the version the row opens | Row click and menu | Row tap and menu |
|
|
| Open another language… | More than one readable version | Menu | Menu |
|
|
| Add translation… | User can create and a language is missing | Menu (dashed chip button as a shortcut) | Menu |
|
|
| Publish / Unpublish <language> | User can publish | Menu | Menu |
|
|
| Copy API URL | Always | Menu | Menu |
|
|
| Delete <language> version | User can delete | Menu | Menu |
|
|
|
|
## Coverage chips for many languages
|
|
|
|
| Width | Chips shown | Overflow | Dashed missing chips |
|
|
| --- | --- | --- | --- |
|
|
| 1440 | 5 | "+3" chip for 8 languages; named "3 more languages: …" | Last in order; buttons only if the user can create; hidden otherwise |
|
|
| 1024 | 4 (narrower column) | "+4" | Same |
|
|
| 390 | 4, under the title | "+4" | Not buttons; added from the row menu |
|
|
| Order | Default language, other existing versions, disabled-language versions, then missing | | Never shown for languages the user can't create |
|
|
|
|
## URL state (article list)
|
|
|
|
| Param | Values | Default |
|
|
| --- | --- | --- |
|
|
| site | Site key | First site |
|
|
| lang | Language code; absent means all languages | all |
|
|
| tab | all, missing, review (Published and Drafts are the status filter) | all |
|
|
| q | Search text | empty |
|
|
| status | online, draft | any |
|
|
| author | Author name | any |
|
|
| seo | attention | any |
|
|
| page | Page number, starting at 1; filter changes reset it | 1 |
|
|
| size | Rows per page | 5 in the fixture |
|
|
|
|
## Public URL pattern validation
|
|
|
|
| Case | Example | Message |
|
|
| --- | --- | --- |
|
|
| Valid | https://journal.example.com/{lang}/{slug} | Preview: https://journal.example.com/fr/comment-nous-chiffrons-une-renovation |
|
|
| Unknown placeholder | https://journal.example.com/{language}/{slug} | Unknown placeholder {language}. Use {lang} and {slug}. |
|
|
| Bad scheme | ftp://journal.example.com/{lang}/{slug} | Start with https:// or http://. |
|
|
| Not encoded | https://journal.example.com/my pages/{lang}/{slug} | Remove spaces and characters that need encoding. Use letters, digits, - and _. |
|
|
| Missing {slug} | https://journal.example.com/{lang} | Add {slug} so each article has its own address. |
|
|
| Unset | | Public URL not configured, with a link to this setting |
|
|
|
|
## Social locale per language
|
|
|
|
| State | Label | Meaning |
|
|
| --- | --- | --- |
|
|
| Derived | Derived: pt_BR | The language tag has a region. Not editable except by setting a value |
|
|
| Set | Set: fr_FR | An owner typed it. Format language_REGION |
|
|
| Not set | Not set | No region available. og:locale and og:locale:alternate are left out |
|
|
| Invalid | fr-FR | Use letters and one underscore, for example fr_FR. |
|
|
|
|
## Move article
|
|
|
|
| Case | Behavior |
|
|
| --- | --- |
|
|
| Entry point | The site select in the editor details |
|
|
| Note | "Moves all language versions of this article." Lists readable versions; a version the user can read but not edit is tagged Read only; unreadable versions are not listed or counted |
|
|
| Confirm | "Move to Atelier?" with the same list |
|
|
| Failures | Language missing in the destination (disabled languages count), address conflict (names only readable conflicting versions), no permission (neutral) |
|
|
|
|
## Release 2 groups
|
|
|
|
| Group | Release | Why |
|
|
| --- | --- | --- |
|
|
| L6 Source reference | Release 2 | Drawn in full so the checker has frames |
|
|
| L9 Freshness and review | Release 2 | Drawn in full so the checker has frames |
|
|
|