Files
RenewCMS/docs/design/states.md
T

56 KiB
Raw Blame History

States

Every state in the three boards is drawn in light and dark (theme switch) at 1440×900 and 390×844. Counts are in README.md.

Clock-sensitive states

Only individual states that render a relative time or a countdown are marked "Clock-sensitive" on the board: Articles "Published" (Updated reads "Just now"), Media "Upload: too many requests (429)" and Settings "Too many requests (429)" (countdowns), Users "Renew invite: done", and the Settings update states that show "Last checked today at 09:41" or "yesterday" (Up to date, Check failed, Update available, Update available with notes open, Update done, Plain restart: confirm, Admin view, Role changed, Update already running). Capture and test them with a frozen clock at 13 Oct 2026, 09:41. Frames that merely list dated rows are not marked.

Sites and authors: roles

Permission model: access follows the role's permissions (Manage sites, Manage authors). The built-in Owner and Admin roles hold them, so the copy below still says "owner" and "owner or admin", as the app ships it. Moving the copy to permission wording is a separate app change (proposal).

Sites: only the owner can create, edit, delete or set the default; other roles have no Sites entry and get a 403 page on a direct link. Anyone who can edit an article can set its site and read the site list. Authors: owner and admin manage; other roles see the list and detail read-only and can create and edit their own author only. The site key is immutable after creation.

Copy glossary

Use Not
Delete (removes a record: article, variant, image, invite, cache files) Remove
Remove link, Remove from the article, Remove (detaches; the record stays) Delete
Create, Add variant, Create draft New (except the page and button name "New article")
Save (a form or the editor), Save changes (edit forms) Update (Update is the software-update action only)
Cancel on the left, primary on the right Close on a dialog that has an action
Close only when the dialog has no action Done, OK
Explicit non-destructive label in place of Cancel when Cancel would be ambiguous: "Keep invite", "Keep key", "Restart later" Cancel there
Sentence case everywhere Title Case
Errors say what happened and what was kept, end with a full stop Exclamation marks
Toast: title is a short result in the past tense ("Article deleted", "Download started"), body one sentence with a full stop Titles with a full stop; future tense
In-flight verb ends in an ellipsis character: "Saving…", "Creating…", "Preparing download…" Three dots

Glossary pass 2, changes made: activity log lines "Removed N expired invites" and "Removed N generated files" became "Deleted …" (they delete records or files); Activity export footer "Close" next to "Download again" became a single "Close"; export title "Export ready" became "Download started"; export failure title "Couldn't prepare the export" became "Couldn't prepare the download"; "Cancel export" became "Cancel". Checked by search across all frames and boards for exclamation marks (none found), Remove and Delete, Update and Save, "New", and dialog footers. Not re-read line by line; see README "Not done".

HTTP status to copy

Status Copy
400 "That request wasn't valid. Check the fields and try again."
401, 403 Global redirect, no local copy (403 from an unverified account). Role 403s have local copy: see the rows below
404 "That item no longer exists."
409 "This changed somewhere else. Reload and try again."
413 "That file is too large."
429 "Too many requests. Try again in a few minutes."
500, network "Something went wrong on our side. Nothing was changed. Try again in a moment."
400 invalid_site_url "Use https. For example https://blog.example.com." / "Remove the path. Use only the host, like https://blog.example.com." / "Enter a full URL, like https://blog.example.com." (inline under base url)
409 site_key_taken "A site with the key "journal" already exists. Choose another key." (inline under key, with a suggestion)
409 site_has_articles "Choose where the articles go. This site has 38 articles." (inside the delete dialog)
403 owner_only (page; raised when the role lacks Manage sites) "You don't have access to Sites" / "Only the owner can manage sites. Ask the owner of this site if you need a change." / "Back to Settings" (403 page on /settings/sites)
403 owner_only (action) "Only the owner can manage sites" / "Ask the owner of this site." (error toast, role changed while the page was open)
403 author_rights "Only an owner or admin can change authors" / "You can still edit your own author profile." (error toast, role changed while the page was open)
400 key_immutable "Used in API paths, so it can't change." (not shown: the edit form has no editable key)
400 invalid_locale "Use a language tag like en or fr-CH." (inline under locale)
403 default_site "The default site can't be deleted. Make another site the default first." (toast if the menu was stale)
404 site_not_found "That site no longer exists" / "It was deleted somewhere else, so we removed it from the list." (info toast)
409 author_slug_taken "The slug "camille-roche" is already used by another author." (inline under slug, with a suggestion)
409 author_has_articles "Can't delete this author" / ""Camille Roche" is credited on 3 articles. Choose another author to credit instead, or remove Camille Roche from those articles first." (server fallback dialog, drawn once)
409 user_already_linked "That user already has an author. Choose another user." (inline under linked user)
422 bio_too_long "Shorten the bio to 280 characters." (inline under bio)
422 invalid_url "Enter a full URL starting with https://." (inline under website and each social link)
404 author_not_found "That author no longer exists" / "It was deleted somewhere else, so we removed it from the list." (info toast)
404 site_not_found (sitemap) GET /v1/site/{key}/sitemap, unknown key. The body is the code site_not_found, shown on Integration in the code style (not rewritten as prose).
400 (sitemap page or size) "The request cannot be processed due to a mismatch in the format of the body." (same text as the other routes on Integration)
403 owner_only vs article SEO Integration note under the 403 row: "Site SEO defaults are owner only. Article SEO fields need only the right to edit the article."
500 – "Couldn't save the site. Your changes are still here. Try again in a moment." / "Couldn't save the author. Your changes are still here. Try again in a moment." / "Couldn't create the author" / "The article is unchanged. Try again in a moment." (notice inside the panel or dialog)
404 content_type_not_found Page: "Content type not found · It may have been deleted, or the address may be mistyped."
404 entry_not_found Page: "Entry not found · It may have been deleted by someone else."
409 content_type_key_taken Under the key: "The key "events" is already used by another content type. Choose a different key."
409 content_type_has_entries Delete dialog: "Recipes now has entries · Someone added an entry after this page loaded, so nothing was deleted. Delete the entries first, then try again."
400 invalid_content_type_key Under the key: "Use 2 to 40 lowercase letters, digits or hyphens."
400 invalid_field On the marked field, per rule (label, key, limits, default, choices). If the client missed one: "Check the fields marked in the list."
409 schema_incompatible Refusal dialog: "Nothing was changed · RenewCMS refused the changes because some entries don't fit the new fields. It doesn't say which entries."
422 invalid_entry: key: required "Fill in Title." (date: "Pick a date for Starts on.", choice: "Choose an option for Category.")
422 invalid_entry: key: type "Title must be text." / "must be a number." / "must be yes or no." / "must be a date, like 2026-11-21."
422 invalid_entry: key: min, max Text: "needs at least N characters", "can be up to N characters". Number: "must be at least N", "must be at most N". Date: "can't be before D", "can't be after D"
422 invalid_entry: key: options "Choose one of: Talk, Workshop or Meetup."
409 invalid_entry: key: unique "Another entry already uses this Title. Enter a different one." plus the alert "Not saved"
422 invalid_entry: key: unknown_field Alert "The fields of Events changed", button Reload form

Public image 400 bodies

{ "status": 400, "error": "unknown_variant", "message": "Unknown variant", "detail": "hero" } and { "status": 400, "error": "unsupported_parameter", "message": "Unsupported parameter", "detail": "width" }. The original is not served as a fallback.

Articles and Media pagination

Same states and copy on Articles and Media (Media draws them under the grid or list). Loading next page keeps row height, controls disabled, summary "Loading page 7…". Error "Couldn't load page 7. Showing page 6." with Retry. Out of range "That page doesn't exist. Showing the last page." Page size menu offers 5, 25, 50, 100. Invalid page or size in the URL (size=30, page=abc, page=0) is replaced by the default without a notice; the API 400 never reaches the user.

Sites and authors pagination

Groups "Sites: pagination" and "Authors: pagination", 10 states each, same pager as Articles and Media, sizes 5, 25, 50, 100 (default 25). First, middle and last page; single page (count line "N sites" or "N authors" only, no pager, no size select); page-size menu; loading ("Loading page N…", skeleton rows, pager kept and disabled); error ("Couldn't load page N. Showing page M." with Retry, rows and page number kept); empty after search (existing empty state, no pager); search resets to page 1; long labels on mobile. Out of range: ?page past the end is replaced by the last page, with "That page doesn't exist. Showing the last page." (not drawn; as Articles). Details in screens/sites.md and screens/authors.md.

Editor

Counter without limit ("1,204 words · 7,318 characters"; at 120,000 characters "19,845 words · 120,000 characters"), "Saving…", "Saved", "Couldn't save. Your text is kept. Retry". Link states and the toolbar overflow are drawn in the Editor group (11 states).

Activity export

Only CSV is offered. "Preparing download…", "Download started", "Couldn't prepare the download", "Nothing to export", "You can't export all activity". The CSV/JSON format control is drawn once, labelled proposed.

Media cleanup, Settings navigation, Activity toggles

As in screens/settings.md, screens/image-variants.md and screens/activity.md. Sync rule for filter cards: card pressed, chip present and the Outcome segment on Failed are one state; removing any removes all three.

Per-screen copy

screens/.md, section "Copy per state" (generated from the board source).

SEO round

Frames: Editor: search and sharing (24), Editor: what the frontend gets (6), Sites: SEO defaults (5), Articles: search status (3). Each is drawn desktop and mobile; light and dark follow the theme switch. None is clock-sensitive: dates in the API preview are fixed sample values. Disabled state: all fields at 40% while saving. Validation: slug taken, canonical not absolute, title template without %title%, excerpt over 300. Suggestions never block saving or publishing. Copy per state: screens/editor.md, articles.md, sites.md.

Custom content types

Groups on RenewCMS Screens Schema.dc.html: 1 Schema list (6), 2 Schema builder (16), 3 Review and delete (7), 4 Field types (6), 5 Content and entry list (9), 6 Entry editor (13), 7 Relations (4), 8 Permissions (2), 9 Export and import (3). 66 states, 56 available now and 10 later. Each has a 1440 and a 390 frame; light and dark follow the theme switch. Nothing is clock-sensitive. Tag per state: "Available now" or "Later". Copy per state: screens/schema.md, content.md, entry.md, schema-later.md.

Roles: owners and admins see Schema and edit types. Any verified user sees the Content group and reads and writes entries. A user who is not owner or admin and opens /schema gets "You don't have access to Schema".

Keys: a type key is 2 to 40 lowercase letters, digits and hyphens (chosen at creation, never changes). A field key is lowercase letters, digits and underscores, starts with a letter, up to 40 (editable until its field is first saved). The two inputs show their own rule.

Schema changes with entries: Save opens the review ("Add 2 fields, change 1, remove 1"); Apply sends the PUT; schema_incompatible shows "Nothing was changed" and names the kind of change that likely caused it, never an entry. A type with entries cannot be deleted ("You can't delete Events yet").

Entry rules, 422 except unique (409): required, type, min, max, options, unique, unknown_field. Messages are per type and drawn in the field-type gallery (group "Field types").

Media field (pass 5)

Copy and states are in media-field.md (sections 2 to 6). Entry rules for a media field: required 422 "Pick an image for Thumbnail."; type 422 "Thumbnail must be an image from the library."; min, max, options and unique do not apply. A required media field added to a type with entries is refused client side in the review ("Can't be applied") and by the server as 409 schema_incompatible. Image deleted while entries use it: the control shows "This image is no longer in the library"; saving is refused only when the field is required. HTTP statuses for the picker and the field use the table above (400, 401, 403, 404, 409, 413, 429, 500, network).

Roles and permissions

90 states, all Available now, listed with trigger and copy in screens/roles.md. Groups: Roles list 7, Role editor 15, Plain-language preview 4, Delete role 4, Users 9, Role-aware shell 8, No access 3, Activity 5, Profile 3, API keys 1, Create-a-role flow 6, Toasts 6, Inline errors 9, Read = Own 5, 409 conflicts 5. None is clock-sensitive (times are fixed strings).

Hidden or disabled:

Case Treatment Example
Whole area the role never has Hidden (no sidebar item, no page card). Direct link shows the 403 page. Schema for a Writer
Action on something visible, blocked by role Disabled with a reason in a tooltip or row text Delete on an article for a Writer
Action that only exists above their level Hidden Publish for a Writer; status shows Draft and "Ask an editor to publish."
Row the person can read but not edit (Own) View only mark on the row; Edit and Delete disabled in the menu You can only edit content you created.
Settings page they can read Read-only: locked fields, banner, Save off Admin on General without Edit general settings
Owner-only action visible to an admin Disabled, Owner only tag Apply update

No-escalation feedback:

Rule Where it shows Copy
Grant only what you hold Disabled segment or switch with a lock; popover on focus or hover You can only grant what your role has. Your role can publish only entries you created, so All isn't available.
No role above yours Disabled option in the role picker (Change role, Delete reassign, Invite) Your role can't assign a role with more power than yours.
No editing your own role Editor opens read-only with a banner You can't change the permissions of your own role. Ask another owner or admin.
No delete while it has members Delete dialog, blocked state 4 members have this role. Move them to another role first. Then Contributor can be deleted.
Owner-only list Disabled item with an Owner only tag and a lock Only the owner can apply updates.

HTTP status to copy (additions):

Code Status Title Body
missing_permission 403 You don't have permission to do that Your role doesn't include {permission name}. Ask an owner or admin.
role_locked 403 This role is locked The Owner role can't be changed or deleted. Transfer ownership from Users instead.
role_has_members 409 Role still has members {n} members have this role. Move them to another role first.
role_name_taken 409 Name already used A role named "{name}" already exists. Choose a different name.
role_conflict 409 This role changed while you were editing {name} saved changes to {role} at {time}. Reload to see them, or review how they differ from your edits.
escalation 403 You can't grant that You can only grant what your role has.
(page level) 403 You don't have access to this page Your role, {role}, doesn't include it. Ask an owner or admin if you need it.

Roles sync (pass 6)

Wording

People are referred to as "they" everywhere: "Their API keys follow the new role." "You can't take ownership back unless they transfer it." No "he", "she", "his" or "her" in any frame or copy table. French uses "vous"; possessives agree with the noun, so they stay neutral ("ses clés API"). Built-in role names stay in English in the French copy until the translation of Owner, Admin and Writer is decided; custom names are user data and are never translated.

Activity event names

Present tense, as the backend emits them. The outcome (succeeded or failed) is a field, so a failed event keeps its name.

Event Sentence Failed sentence
role.create Role Contributor created Failed to create the role Contributor
role.edit Role Contributor edited Failed to edit the role Contributor
role.delete Role Intern deleted Failed to delete the role Intern
role.reassign Moved 3 members from Intern to Writer Failed to move 3 members from Intern to Writer
user.change_role Ines Duarte's role changed from Writer to Content lead Failed to change the role of Ines Duarte
user.transfer_ownership Ownership transferred from Marta Alves to Jonas Weber Failed to transfer ownership to Jonas Weber
Failed template: "Failed to {verb} {object}". Category names in the filter: Articles, Media, Users and invites, Roles, API keys, Authentication, System. French for every string is in the board table "Final copy: toasts and errors, EN and FR" and in screens/roles.md.

Stale state

Nothing is changed when a request is stale. The message names who or what changed, says nothing was changed, and ends with the recovery. Where it shows: banner in the role editor, alert inside a dialog, toast only when the surface is no longer open. The form keeps what the person entered. Recovery: Reload (toast), Reload role or Review differences (editor), Reload member (transfer), retry (change role).

Case Status Where Copy
Stale role save 409 role_conflict Banner in the editor This role changed while you were editing · {name} saved changes to {role} at {time}. Reload to see them, or review how they differ from your edits.
Change of role, member's role changed first 409 Alert in the dialog, or toast if it closed Role changed in the meantime · Ines Duarte is now Admin. Your choice is kept. Check it and try again.
Transfer, target's role changed first 409 Alert in the dialog, or toast if it closed Role changed in the meantime · Jonas Weber is now Writer. What you typed is kept. Check the change, then try again.
Removal, member's role changed first 409 Toast User not removed · Ines Duarte's role changed in the meantime, so nothing was removed. Check it and try again.
Role deleted by someone else 404 Info toast That role no longer exists · It was deleted somewhere else, so we removed it from the list.

Read = Own

A role with Read set to Own sees online articles plus its own drafts. For custom content types it sees only entries it created. Counts, totals and pagination follow the same rule, so hidden items are not hinted at. Update or Delete set to Own still list other people's rows with the View only mark. Frames: group "14 Read = Own" on the Roles board.

Wording status

Strings that match messages.ts are verbatim. "Only the owner can manage sites…" and other board-only wording are proposals. Moving the app to permission-based wording is a separate code change.

Account: change email, export, delete

States and copy in screens/account.md. Board: RenewCMS Screens Account.dc.html (33 states) and the Auth board groups "Verify new email" (7) and "Account deleted" (1). Direct-download rate limit state is clock-sensitive (frozen clock 09:41).

Status Copy
403 wrong_password "That password isn't correct. Try again." (inline under password)
429 existing 429 copy, inline in the form or the dialog
500, network existing 500 copy, or "Couldn't delete your account. Nothing was deleted. Try again in a moment."
409 last_admin "You can't delete your account yet" (non-dismissable dialog)
409 email_unavailable Landing only: "We couldn't change your email address" (same for in use and rejected)

Localization state IDs (groups L1-L12 on the Localization board; source of truth: locale-spec.js)

Every state has a stable ID, a frame selector and a fixture. Legacy IDs from the first pass keep their names and are the canonical ID of the state they map to; the loc.lN.xxx alias is listed below.

Total: 169 states (was 109).

L1 Site languages

Route: /settings/sites/:key/languages API: proposed: GET/PUT /v1/site/{key}/languages

Navigation: Settings → Sites → [site] → Languages only. The site name is in the heading and breadcrumb. Site formatting locale, default content language, article language and social locale are four separate values. At least one language stays enabled and the default must be enabled: to retire the default, first Make default on another language, then Disable. Disable and Remove never delete or unpublish content. Disabled languages show a Disabled tag and a Re-enable action; their versions stay readable and editable per permission. Drafts can be published only after Re-enable ("French is disabled on this site. Re-enable it to publish."). The language filter lists them under Disabled. Social locale per language has three states: Derived (from the language code, e.g. pt-BR gives pt_BR), Set (an owner typed it, e.g. fr_FR) and Not set (og:locale left out). Only owners edit it.

ID State Trigger Selector Sizes Fixture
loc.l1.single Single language Owner opens Languages on Archive (one language, pt-BR kept) LocaleFrame{s:{v:"sl",x:"single"}} 1440, 390 Archive
loc.l1.multi Multilingual Journal: four enabled, two disabled (Spanish, Italian) LocaleFrame{s:{v:"sl",x:"multi"}} 1440, 1024, 390 Journal
loc.l1.loading Loading Request in flight LocaleFrame{s:{v:"sl",x:"loading"}} 1440, 390 Journal
loc.l1.error Load failed Network or server error LocaleFrame{s:{v:"sl",x:"error"}} 1440, 390 Journal
loc.l1.add Add a language Add language LocaleFrame{s:{v:"sl",x:"add"}} 1440, 390 Journal
loc.l1.saveFail Add failed Server error, selection kept LocaleFrame{s:{v:"sl",x:"saveFail"}} 1440, 390 Journal
loc.l1.adddup Duplicate language Already enabled LocaleFrame{s:{v:"sl",x:"adddup"}} 1440, 390 Journal
loc.l1.broader Only a broader tag supported Add Portuguese (Portugal): substitution explained, confirmation required LocaleFrame{s:{v:"sl",x:"broader"}} 1440, 390 Journal
loc.l1.default Make default Row menu LocaleFrame{s:{v:"sl",x:"default"}} 1440, 390 Journal
loc.l1.defaultFail Set default failed Server error, nothing changed LocaleFrame{s:{v:"sl",x:"defaultFail"}} 1440, 390 Journal
loc.l1.disable Disable with content Row menu → Disable German LocaleFrame{s:{v:"sl",x:"disable"}} 1440, 390 Journal
loc.l1.stale Languages changed Another owner changed them: "Languages changed. Reload." LocaleFrame{s:{v:"sl",x:"stale"}} 1440, 390 Journal
loc.lang.disableDefault Disable default blocked Disable English LocaleFrame{s:{v:"sl",x:"disableDefault"}} 1440, 390 Journal
loc.lang.disableLast Disable last blocked Only one enabled LocaleFrame{s:{v:"sl",x:"disableLast"}} 1440, 390 Journal
loc.l1.reenable Re-enable Spanish row → Re-enable LocaleFrame{s:{v:"sl",x:"reenable"}} 1440, 390 Journal
loc.l1.reenableDone Re-enabled Toast after success LocaleFrame{s:{v:"sl",x:"reenableDone"}} 1440, 390 Journal
loc.l1.reenableFail Re-enable failed Dialog error, still disabled LocaleFrame{s:{v:"sl",x:"reenableFail"}} 1440, 390 Journal
loc.l1.remove Remove (no content) Italian (0 versions) LocaleFrame{s:{v:"sl",x:"remove"}} 1440, 390 Journal
loc.l1.removeFail Remove failed Dialog error, still listed LocaleFrame{s:{v:"sl",x:"removeFail"}} 1440, 390 Journal
loc.l1.removeBlocked Remove blocked Spanish has versions LocaleFrame{s:{v:"sl",x:"removeBlocked"}} 1440, 390 Journal
loc.l1.readonly Read-only view Non-owner: list and social locale readable; no controls LocaleFrame{s:{v:"sl",x:"readonly"}} 1440, 390 Journal
loc.l1.saved Saved After Add language LocaleFrame{s:{v:"sl",x:"saved"}} 1440, 390 Journal
loc.l1.social Social locale: three states Atelier: Derived en_GB and pt_BR, Set fr_FR, Not set LocaleFrame{s:{v:"sl",x:"social"}} 1440, 1024, 390 Atelier
loc.l1.socialEdit Social locale: edit Edit on French (Set: fr_FR) LocaleFrame{s:{v:"sl",x:"socialEdit"}} 1440, 390 Atelier
loc.l1.socialInvalid Social locale: invalid fr-FR typed with a hyphen LocaleFrame{s:{v:"sl",x:"socialInvalid"}} 1440, 390 Atelier

L1b Public URL pattern (owner setting)

Route: /settings/sites/:key/public-url API: proposed: GET/PUT /v1/site/{key} { public_url_pattern }

Owner-only. One pattern per site, for example https://example.com/{lang}/{slug}. Supported placeholders: {lang} (language code) and {slug} (the article address in that language). {slug} is required: without it every article would share one address. The preview uses a real article of the site. When unset, article panels show "Public URL not configured" and link here. No URL is invented.

ID State Trigger Selector Sizes Fixture
loc.l1b.set Set Pattern saved, preview shown LocaleFrame{s:{v:"pu",x:"set"}} 1440, 390 Journal
loc.l1b.editing Editing Live preview follows the field LocaleFrame{s:{v:"pu",x:"editing"}} 1440, 390 Journal
loc.l1b.unknown Unknown placeholder {language} typed LocaleFrame{s:{v:"pu",x:"unknown"}} 1440, 390 Journal
loc.l1b.scheme Bad scheme ftp:// typed LocaleFrame{s:{v:"pu",x:"scheme"}} 1440, 390 Journal
loc.l1b.encoded Not encoded A space in the path LocaleFrame{s:{v:"pu",x:"encoded"}} 1440, 390 Journal
loc.l1b.noSlug Missing {slug} Pattern without {slug} LocaleFrame{s:{v:"pu",x:"noSlug"}} 1440, 390 Journal
loc.l1b.saved Saved Toast LocaleFrame{s:{v:"pu",x:"saved"}} 1440, 390 Journal
loc.l1b.unset Unset No pattern; opened from "Public URL not configured" LocaleFrame{s:{v:"pu",x:"unset"}} 1440, 390 Journal
loc.l1b.viewOnly Read-only view Non-owner: pattern readable, no controls LocaleFrame{s:{v:"pu",x:"viewOnly"}} 1440, 390 Journal

L2 First-time setup for existing articles

Route: /settings/sites/:key/languages/setup API: proposed: GET /v1/language/setup, POST /v1/language/setup { sites:[{ key, lang }] } (lang is the full tag, e.g. pt-BR)

Opened from the Languages banner of any site; :key is the site it was opened from, and the page lists every site that still has unlabelled articles. Setup assigns a language per site and applies in one transaction. It never translates, links or moves articles. Each site is seeded from its full Site.Locale when a matching language entry exists: pt-BR becomes Portuguese (Brazil). A site with no locale has no seed and the owner must choose. Nothing applies until the owner confirms every row, including a site with no articles. Retrying is safe.

ID State Trigger Selector Sizes Fixture
loc.l2.ready Ready to assign Journal (en), Notes (no locale, must choose), Archive (pt-BR kept) LocaleFrame{s:{v:"su",x:"ready"}} 1440, 390 Journal, Notes, Archive
loc.l2.noLocale No-locale site Notes only: the owner must choose LocaleFrame{s:{v:"su",x:"noLocale"}} 1440, 390 Notes
loc.l2.empty Site with no articles Gallery has no articles; en-GB kept, confirm and Save LocaleFrame{s:{v:"su",x:"empty"}} 1440, 390 Gallery
loc.l2.applying Applying Request in flight LocaleFrame{s:{v:"su",x:"applying"}} 1440, 390 Journal, Notes, Archive
loc.l2.inProgress Setup already running Another owner started it; starting again is safe LocaleFrame{s:{v:"su",x:"inProgress"}} 1440, 390 Journal, Notes, Archive
loc.l2.done Done 16 articles labelled LocaleFrame{s:{v:"su",x:"done"}} 1440, 390 Journal, Notes, Archive
loc.l2.fail Failed Nothing changed; choices kept; retry is safe LocaleFrame{s:{v:"su",x:"fail"}} 1440, 390 Journal, Notes, Archive

L3 New article

Route: /articles (New article modal) API: existing POST /v1/article/ + proposed lang, story_id

Single-language sites show no language field. Multilingual sites show Language and a New article / Translation of an existing article choice. The translation path uses the same endpoint and rules as Add translation. The picker lists only articles the user can read; the chosen version is the source.

ID State Trigger Selector Sizes Fixture
loc.l3.single Single language Title and site only LocaleFrame{s:{v:"na",x:"single"}} 1440, 390 Archive
loc.l3.multi Multilingual: new article Language defaults to the site default LocaleFrame{s:{v:"na",x:"multi"}} 1440, 390 Journal
loc.l3.translation Translation of an article Article picker + language LocaleFrame{s:{v:"na",x:"translation"}} 1440, 390 Journal
loc.l3.sitechange Site changed Selected language not offered by the new site LocaleFrame{s:{v:"na",x:"sitechange"}} 1440, 390 Journal, Notes

L4 Add a translation

Route: /article/{id}/edit?dialog=add-translation&lang=fr API: proposed: POST /v1/article/{id}/translations

Always creates a draft. Source: the user picks which readable version to copy from (default: the default-language version if readable, else the first readable). The source version is captured in the same transaction as the copy. Copied text stays in the source language until edited; the version starts as Needs review. Start empty: no tracked source, so freshness shows No source. The primary action stays in the sticky footer on mobile; the body scrolls.

ID State Trigger Selector Sizes Fixture
loc.l4.form Copy from source Editor → language menu → Add translation LocaleFrame{s:{v:"at",x:"form"}} 1440, 390 Journal
loc.l4.empty Start empty Title only LocaleFrame{s:{v:"at",x:"empty"}} 1440, 390 Journal
loc.l4.saving Creating Submit in flight LocaleFrame{s:{v:"at",x:"saving"}} 1440, 390 Journal
loc.dup.readable Exists, readable Conflict; Open offered LocaleFrame{s:{v:"at",x:"dup"}} 1440, 390 Journal
loc.dup.hidden Exists, not readable Neutral error, no Open LocaleFrame{s:{v:"at",x:"dupHidden"}} 1440, 390 Journal
loc.l4.fail Failed Server error, entries kept LocaleFrame{s:{v:"at",x:"fail"}} 1440, 390 Journal
loc.l4.gone Source deleted Deleted while the dialog was open LocaleFrame{s:{v:"at",x:"gone"}} 1440, 390 Journal
loc.l4.unreadable Source no longer readable Access lost between opening and submitting LocaleFrame{s:{v:"at",x:"unreadable"}} 1440, 390 Journal
loc.l4.langOff Language disabled mid-flow An owner disabled French while the dialog was open LocaleFrame{s:{v:"at",x:"langOff"}} 1440, 390 Journal
loc.l4.long Long dialog Long title, many authors and images; body scrolls, footer stays LocaleFrame{s:{v:"at",x:"long"}} 1440, 390 Journal

L5 Editor: header, actions and title

Route: /article/{id}/edit (one id per version) API: proposed: GET /v1/article/{id} → translations[], capabilities[]

Header priority and the action matrix are in the tables below. One filled primary button at most: Save and publish when allowed, dirty and offline; otherwise Save when dirty; otherwise Publish when clean and offline. Save stays separate and secondary. Desktop collapse order as width shrinks (1440 → 1024): reference button loses its label, Unpublish moves to the overflow menu, the freshness badge becomes icon-only. Mobile: title, language, publication and dirty badges stay at the top; Save, primary and an overflow button sit in a fixed bottom bar; reference, Unpublish, Edit slug, Move and Delete go to the overflow sheet. The language control is a menu button with a menu (not a listbox).

ID State Trigger Selector Sizes Fixture
loc.l5.default Clean draft Open a draft you can publish; title pristine LocaleFrame{s:{v:"ed",x:"default"}} 1440, 1024, 390 Journal
loc.l5.dirty Dirty draft Can edit, can't publish: Save is primary LocaleFrame{s:{v:"ed",x:"dirty"}} 1440, 1024, 390 Journal
loc.l5.dirtyPublish Dirty draft, publish allowed Save and publish French is primary; Save secondary LocaleFrame{s:{v:"ed",x:"dirtyPublish"}} 1440, 1024, 390 Journal
loc.l5.cleanOnline Clean online Unpublish only; no primary LocaleFrame{s:{v:"ed",x:"cleanOnline"}} 1440, 1024, 390 Journal
loc.l5.onlineDirty Dirty online Save updates the live page; Unpublish stays LocaleFrame{s:{v:"ed",x:"onlineDirty"}} 1440, 1024, 390 Journal
loc.l5.saving Saving Save in flight LocaleFrame{s:{v:"ed",x:"saving"}} 1440, 390 Journal
loc.l5.saved Saved Toast LocaleFrame{s:{v:"ed",x:"saved"}} 1440, 390 Journal
loc.l5.saveFail Save failed Stays unsaved; Try saving again; nothing published LocaleFrame{s:{v:"ed",x:"saveFail"}} 1440, 1024, 390 Journal
loc.l5.pubFail Publish failed after save Saved as a draft; Retry publish; edits shown as saved LocaleFrame{s:{v:"ed",x:"pubFail"}} 1440, 1024, 390 Journal
loc.l5.ovf Overflow menu More actions (sheet on mobile) LocaleFrame{s:{v:"ed",x:"ovf"}} 1440, 390 Journal
loc.l5.menu Menu: four languages Open menu LocaleFrame{s:{v:"ed",x:"menu"}} 1440, 390 Journal
loc.l5.two Menu: two languages Two-language site LocaleFrame{s:{v:"ed",x:"two"}} 1440, 390 Journal
loc.l5.many Menu: many languages Scrolls inside the viewport, with filter LocaleFrame{s:{v:"ed",x:"many"}} 1440, 390 Journal
loc.l5.unsaved Switch with unsaved changes Choose English while dirty LocaleFrame{s:{v:"ed",x:"unsaved"}} 1440, 390 Journal
loc.l5.loading Loading version Slow switch LocaleFrame{s:{v:"ed",x:"loading"}} 1440, 390 Journal
loc.l5.navFail Switch failed Stays on current version LocaleFrame{s:{v:"ed",x:"navFail"}} 1440, 390 Journal
loc.l5.gone Version unavailable Deleted or no access (neutral) LocaleFrame{s:{v:"ed",x:"gone"}} 1440, 390 Journal
loc.l5.focus Title: focus Field focused, not changed LocaleFrame{s:{v:"ed",x:"focus"}} 1440, 390 Journal
loc.l5.titleEdit Title: editing Typing; counter shown LocaleFrame{s:{v:"ed",x:"titleEdit"}} 1440, 390 Journal
loc.l5.titleUnsaved Title: unsaved Changed, focus left LocaleFrame{s:{v:"ed",x:"titleUnsaved"}} 1440, 390 Journal
loc.l5.invalid Title: invalid Empty: "Enter a title" LocaleFrame{s:{v:"ed",x:"invalid"}} 1440, 390 Journal
loc.l5.titleShort Title: too short Under 3 characters LocaleFrame{s:{v:"ed",x:"titleShort"}} 1440, 390 Journal
loc.l5.titleLong Title: too long Over 200 characters LocaleFrame{s:{v:"ed",x:"titleLong"}} 1440, 390 Journal
loc.l5.titleSaving Title: saving Save in flight LocaleFrame{s:{v:"ed",x:"titleSaving"}} 1440, 390 Journal
loc.l5.failed Title: save failed Text kept, still unsaved, Try again LocaleFrame{s:{v:"ed",x:"failed"}} 1440, 390 Journal

L5b Permission states (per version)

Route: /article/{id}/edit API: capabilities[] on GET /v1/article/{id}; every PATCH/DELETE rechecked on the server

Every action renders from its own capability (read, edit, publish, delete, create translation, review). See the capability matrix. Missing-translation rows appear only for users who can create; unreadable siblings are absent with no placeholder and no count. The formatting toolbar is hidden whenever edit is missing.

ID State Trigger Selector Sizes Fixture
loc.perm.readonly P1 Read-only version Banner, read-only fields, no Save, Publish, Delete, toolbar; reference stays LocaleFrame{s:{v:"perm",x:"ro"}} 1440, 1024, 390 Journal
loc.l5b.roMenu P1 menu No Add translation row LocaleFrame{s:{v:"perm",x:"roMenu"}} 1440, 390 Journal
loc.perm.editNoPublish P2 Edit, no publish Save only; hint about the publish permission LocaleFrame{s:{v:"perm",x:"p2"}} 1440, 1024, 390 Journal
loc.perm.publishNoEdit P3 Publish, no edit Publish only; fields read-only LocaleFrame{s:{v:"perm",x:"p3"}} 1440, 1024, 390 Journal
loc.perm.siblingReadOnly P4 Sibling readable, not editable Arabic row tagged Read only LocaleFrame{s:{v:"perm",x:"p4"}} 1440, 390 Journal
loc.perm.siblingHidden P5 Sibling not readable German absent, no count, no placeholder LocaleFrame{s:{v:"perm",x:"p5"}} 1440, 390 Journal
loc.perm.createNotAll P6 Can create, not modify all Delete article and Move disabled with the reason; Delete French version available LocaleFrame{s:{v:"perm",x:"p6"}} 1440, 390 Journal
loc.l5b.noAdd Menu without Add translation No create permission; Arabic is missing but offers nothing LocaleFrame{s:{v:"perm",x:"noAdd"}} 1440, 390 Journal
loc.l5b.withAdd Menu with Add translation Same article, user may create LocaleFrame{s:{v:"perm",x:"withAdd"}} 1440, 390 Journal

L6 Source reference (release 2) (release 2)

Route: /article/{id}/edit?ref=:sourceId API: proposed: GET /v1/article/{id} (read-only fetch of the reference)

Release 2. The temporary reference (?ref=, dashed outline, "Temporary reference") and the tracked review source (solid "Tracked source" badge in the review banner) are separate: changing the reference never changes the tracked source. Show source reference is in the editor header (desktop, icon-only at 1024) and the overflow sheet (mobile). Each pane scrolls independently. dir and lang go on the title and body of the editing pane and of the reference, never on the app shell.

ID State Trigger Selector Sizes Fixture
loc.l6.split Side by side Show source reference LocaleFrame{s:{v:"src",x:"split"}} 1440, 1024, 390 Journal
loc.l6.closed Reference hidden Hide source reference; button returns LocaleFrame{s:{v:"src",x:"closed"}} 1440, 390 Journal
loc.l6.reopen Reopened Scroll positions kept LocaleFrame{s:{v:"src",x:"reopen"}} 1440, 390 Journal
loc.l6.changeRef Change reference Menu; tracked source stays English LocaleFrame{s:{v:"src",x:"changeRef"}} 1440, 390 Journal
loc.l6.narrow Narrow desktop (1024) True 1024 frame LocaleFrame{s:{v:"src",x:"narrow"}} 1024, 390 Journal
loc.l6.long Long content Both panes scroll independently LocaleFrame{s:{v:"src",x:"long"}} 1440, 390 Journal
loc.l6.rtl Arabic editing, English reference dir=rtl on the Arabic title and body only LocaleFrame{s:{v:"src",x:"rtl"}} 1440, 390 Journal
loc.l6.arRef French editing, Arabic reference dir=rtl on the reference pane only LocaleFrame{s:{v:"src",x:"arRef"}} 1440, 390 Journal
loc.l6.mobileEdit Mobile: editing Segment LocaleFrame{s:{v:"src",x:"mobileEdit"}} 1440, 390 Journal
loc.l6.mobileSrc Mobile: source Segment LocaleFrame{s:{v:"src",x:"mobileSrc"}} 1440, 390 Journal
loc.l6.mobileClosed Mobile: closed Show source reference in the bottom bar or sheet LocaleFrame{s:{v:"src",x:"mobileClosed"}} 1440, 390 Journal
loc.l6.unavail Reference unavailable Deleted or unreadable LocaleFrame{s:{v:"src",x:"unavail"}} 1440, 390 Journal

L7 Article list and coverage

Route: /articles?site&lang&tab=all|missing|review&q&status&author&seo&page&size API: proposed: lang, tab=all|missing|review on GET /v1/article/ (same names as the URL)

Journal: 8 articles, 17 versions, page size 5. The list header shows the unit: "8 articles" (all languages, one row per article with its translations), "4 French versions" (language selected), "4 articles not in French" (missing tab), "7 articles need translation", "2 articles need review". Coverage chips: default language first, then other existing versions, then disabled-language versions, then dashed missing chips (only for languages the user can create). Chips collapse after 5 at 1440 and 4 at 1024 and 390, leaving a "+N" chip. Row actions are the same on every width: Open, Open another language, Add translation, Publish or Unpublish, Copy API URL, Delete version; every row has the row menu button. All state is in the URL.

ID State Trigger Selector Sizes Fixture
loc.l7.all All languages, page 1 5 of 8 articles LocaleFrame{s:{v:"li",x:"all"}} 1440, 1024, 390 Journal
loc.l7.page2 All languages, page 2 (last) 3 of 8 articles LocaleFrame{s:{v:"li",x:"page2"}} 1440, 1024, 390 Journal
loc.l7.fr French selected 4 French versions LocaleFrame{s:{v:"li",x:"fr"}} 1440, 1024, 390 Journal
loc.l7.frMissing Not in French 4 articles not in French LocaleFrame{s:{v:"li",x:"frMissing"}} 1440, 1024, 390 Journal
loc.l7.emptyMissing Nothing missing Press: all articles translated; no summary, no pager LocaleFrame{s:{v:"li",x:"emptyMissing"}} 1440, 1024, 390 Press
loc.l7.noResults No results Search "zinc"; every count 0 LocaleFrame{s:{v:"li",x:"noResults"}} 1440, 1024, 390 Journal
loc.l7.frLoading Loading Counts shown as – LocaleFrame{s:{v:"li",x:"frLoading"}} 1440, 1024, 390 Journal
loc.l7.listFail Failed Filters kept LocaleFrame{s:{v:"li",x:"listFail"}} 1440, 1024, 390 Journal
loc.l7.limited Limited access Reads English only, can't create: no dashed chips LocaleFrame{s:{v:"li",x:"limited"}} 1440, 1024, 390 Journal (limited scope)
loc.l7.review Needs review tab 2 articles need review LocaleFrame{s:{v:"li",x:"review"}} 1440, 1024, 390 Journal
loc.l7.author Author filter Marta Alves: 3 articles, chips and Clear all LocaleFrame{s:{v:"li",x:"author"}} 1440, 1024, 390 Journal
loc.l7.langFilter Language filter Enabled and Disabled groups LocaleFrame{s:{v:"li",x:"langFilter"}} 1440, 1024, 390 Journal
loc.l7.rowMenu Row menu Same actions on desktop and mobile LocaleFrame{s:{v:"li",x:"rowMenu"}} 1440, 1024, 390 Journal
loc.l7.filters Mobile filter sheet Journal, French, Draft: 2 results LocaleFrame{s:{v:"li",x:"filters"}} 1440, 1024, 390 Journal
loc.l7.tabsScroll Tabs scrolled Not in French reachable; fade and chevrons LocaleFrame{s:{v:"li",x:"tabsScroll"}} 1440, 390 Journal
loc.l7.long Eight languages, long labels, filters Atelier: chips collapse to +3, +4, +4; Draft and SEO filters LocaleFrame{s:{v:"li",x:"long"}} 1440, 1024, 390 Atelier

L8 Publishing and deletion

Route: same as the editor API: existing PATCH /v1/article/{id}/publish and /unpublish and DELETE per article id

Every action names its language. No bulk publish. With unsaved edits the primary reads Save and publish and saves first; if the save fails nothing is published and the edits stay unsaved; if the save works and publishing fails the edits show as saved. Unpublish never saves: the saved version goes offline and local edits stay in the editor. Saving an online version updates the live page immediately, and the editor says so. Deletion confirmations name only readable versions.

ID State Trigger Selector Sizes Fixture
loc.l8.publish Publish French Confirm, English untouched LocaleFrame{s:{v:"pd",x:"publish"}} 1440, 390 Journal
loc.lang.publishDisabled Publishing blocked: language disabled French disabled on the site LocaleFrame{s:{v:"pd",x:"publishDisabled"}} 1440, 390 Journal
loc.l8.saveAndPublish Save and publish French Unsaved edits: saves first, then publishes LocaleFrame{s:{v:"pd",x:"saveAndPublish"}} 1440, 390 Journal
loc.l8.saveFailPublish Save failed, nothing published Toast; edits kept and retryable LocaleFrame{s:{v:"pd",x:"saveFailPublish"}} 1440, 390 Journal
loc.l8.unpublish Unpublish French Confirm LocaleFrame{s:{v:"pd",x:"unpublish"}} 1440, 390 Journal
loc.l8.unpublishDirty Unpublish with unsaved edits Saved version goes offline; edits kept; nothing saved LocaleFrame{s:{v:"pd",x:"unpublishDirty"}} 1440, 390 Journal
loc.l8.delete Delete French version Confirm LocaleFrame{s:{v:"pd",x:"delete"}} 1440, 390 Journal
loc.l8.deleteSource Delete a source version Warns about review tracking LocaleFrame{s:{v:"pd",x:"deleteSource"}} 1440, 390 Journal
loc.l8.deleteLast Delete the only version Deletes the article LocaleFrame{s:{v:"pd",x:"deleteLast"}} 1440, 390 Journal
loc.l8.deleteStoryOk Delete article (all readable versions) Names readable versions only LocaleFrame{s:{v:"pd",x:"deleteStoryOk"}} 1440, 390 Journal
loc.l8.deleteStory Delete article blocked Neutral, no version named LocaleFrame{s:{v:"pd",x:"deleteStory"}} 1440, 390 Journal
loc.l8.done Published Toast LocaleFrame{s:{v:"pd",x:"done"}} 1440, 390 Journal
loc.l8.fail Publish failed Toast LocaleFrame{s:{v:"pd",x:"fail"}} 1440, 390 Journal

L9 Freshness and review (release 2) (release 2)

Route: editor banner · list badge API: proposed: POST /v1/article/{id}/review { source_article_id, source_revision, translation_revision } (internal fields; never shown)

Release 2. Publication (solid badge) and freshness (outlined badge with refresh icon) are separate. Labels: No version, Draft, Needs review, Reviewed. A copied draft starts as Needs review. The dialog shows "Changed fields: Title, Body", the source version with its date and the translation version with its date. No revision numbers appear in the UI.

ID State Trigger Selector Sizes Fixture
loc.l9.warn Source updated English changed after review LocaleFrame{s:{v:"fr",x:"warn"}} 1440, 390 Journal
loc.l9.pub Published, needs review Still online LocaleFrame{s:{v:"fr",x:"pub"}} 1440, 390 Journal
loc.l9.review Mark reviewed Confirm with changed fields and dates LocaleFrame{s:{v:"fr",x:"review"}} 1440, 390 Journal
loc.review.changed Source changed again During the review LocaleFrame{s:{v:"fr",x:"changed"}} 1440, 390 Journal
loc.l9.changedTr Translation changed Saved by someone else while confirming LocaleFrame{s:{v:"fr",x:"changedTr"}} 1440, 390 Journal
loc.l9.replace Choose tracked source Cycle prevented LocaleFrame{s:{v:"fr",x:"replace"}} 1440, 390 Journal
loc.src.inaccessible Source unavailable (access) History kept LocaleFrame{s:{v:"fr",x:"inaccessible"}} 1440, 390 Journal
loc.src.deleted Source deleted Choose a new source LocaleFrame{s:{v:"fr",x:"gone"}} 1440, 390 Journal
loc.l9.done Reviewed Toast LocaleFrame{s:{v:"fr",x:"done"}} 1440, 390 Journal
loc.l9.fail Review failed Toast LocaleFrame{s:{v:"fr",x:"fail"}} 1440, 390 Journal
loc.l9.frReview Published and needs review in the list French tab, Needs review: 2 versions LocaleFrame{s:{v:"li",x:"frReview"}} 1440, 1024, 390 Journal

L10 Metadata and public API

Route: Search and sharing panel · /integration API: existing slug, SEO fields; proposed alternates[], lang; exact lookup by site + lang + slug; resolution by story_id

Slug is unique per (site, lang). Exact lookup is by site + language + slug and never falls back. Language resolution and fallback use the stable story_id. Website URLs come from the site's Public URL pattern; without it the panel says "Public URL not configured" and links to the setting. Canonical is the localized page itself unless an override is set. SEO labels: "Fallback: French title", "No fallback", "Empty. English site default not used."

ID State Trigger Selector Sizes Fixture
loc.seo.noFallback Search and sharing: French Localized slug, canonical, alternates, fallback labels, Edit slug LocaleFrame{s:{v:"seo",x:""}} 1440, 390 Journal
loc.slug.published Edit slug Published version: warning about old links LocaleFrame{s:{v:"seo",x:"editSlug"}} 1440, 390 Journal
loc.url.unset Public URL not configured No pattern, no fabricated URL LocaleFrame{s:{v:"seo",x:"unset"}} 1440, 390 Journal
loc.l10.noSocial Social locale not set fr has no region and none is set: og:locale omitted LocaleFrame{s:{v:"seo",x:"noSocial"}} 1440, 390 Journal
loc.l10.api Integration: languages Exact lookup, resolve by article group, errors LocaleFrame{s:{v:"api",x:""}} 1440, 390 Journal

L11 Conflicts and sessions

Route: editor API: existing 401/403/404/409 patterns

The conflict dialog gives four choices. Keep my changes is the only filled button; the other three are secondary and equal. "Someone else saved this version at

ID State Trigger Selector Sizes Fixture
loc.l11.session Session expired Draft kept locally LocaleFrame{s:{v:"pm",x:"session"}} 1440, 390 Journal
loc.l11.restored Restored after login Editor shows "Restored unsaved changes from 14:18" LocaleFrame{s:{v:"pm",x:"restored"}} 1440, 390 Journal
loc.conflict Concurrent update Four choices, local work kept LocaleFrame{s:{v:"pm",x:"conflict"}} 1440, 390 Journal
loc.l11.conflictRestored Concurrent update after session expiry Restored changes notice above the choices LocaleFrame{s:{v:"pm",x:"conflictRestored"}} 1440, 390 Journal
loc.l11.reloadConfirm Reload theirs: confirm "This discards your unsaved changes." LocaleFrame{s:{v:"pm",x:"reloadConfirm"}} 1440, 390 Journal
loc.l11.conflict2 Second conflict Save over theirs failed again; nothing overwritten LocaleFrame{s:{v:"pm",x:"conflict2"}} 1440, 390 Journal
loc.l11.download Download started Toast; changes still in the editor LocaleFrame{s:{v:"pm",x:"download"}} 1440, 390 Journal

L11b Move article to another site

Route: editor → site select API: proposed: POST /v1/story/{story_id}/move { site }

Entry point is the site select in the editor details. Articles move as a whole, only when the destination enables every language they contain (disabled ones included) and the user can edit all versions. The note lists readable versions only; versions the user can't read are not listed or counted. Moves are atomic and revalidate permissions and conflicts in the transaction.

ID State Trigger Selector Sizes Fixture
loc.l11b.moveSelect Site select Note lists all four versions, one tagged Read only LocaleFrame{s:{v:"pm",x:"moveSelect"}} 1440, 390 Journal
loc.l11b.moveSelectHidden Site select, hidden version Arabic not readable: not listed or counted LocaleFrame{s:{v:"pm",x:"moveSelectHidden"}} 1440, 390 Journal
loc.l11b.moveConfirm Confirm Move to Atelier? LocaleFrame{s:{v:"pm",x:"moveConfirm"}} 1440, 390 Journal
loc.l11b.moveBusy In progress Dialog locked LocaleFrame{s:{v:"pm",x:"moveBusy"}} 1440, 390 Journal
loc.l11b.moveDone Moved Toast LocaleFrame{s:{v:"pm",x:"moveDone"}} 1440, 390 Journal
loc.l11b.move Language missing in destination Notes enables English only LocaleFrame{s:{v:"pm",x:"move"}} 1440, 390 Journal, Notes
loc.l11b.moveSlug Address conflict Names the conflicting readable version only LocaleFrame{s:{v:"pm",x:"moveSlug"}} 1440, 390 Journal, Atelier
loc.l11b.movePerm No permission Neutral LocaleFrame{s:{v:"pm",x:"movePerm"}} 1440, 390 Journal

L12 Existing surfaces

Route: Home · Activity · Media · editor details API: existing; language added where it applies

Single-language sites show none of this. Home counts articles, not versions.

ID State Trigger Selector Sizes Fixture
loc.l12.home Home: recent articles Default-language title; code only when not default LocaleFrame{s:{v:"sf",x:"home"}} 1440, 1024, 390 Journal
loc.l12.homeFr Home: reader of French only Titles in French with code FR LocaleFrame{s:{v:"sf",x:"homeFr"}} 1440, 1024, 390 Journal
loc.l12.activity Activity Events name the language LocaleFrame{s:{v:"sf",x:"activity"}} 1440, 1024, 390 Journal
loc.l12.media Media usage Shared image counted once, versions listed LocaleFrame{s:{v:"sf",x:"media"}} 1440, 1024, 390 Journal
loc.l12.menu Article menu labels Publish and Delete name the language LocaleFrame{s:{v:"sf",x:"menu"}} 1440, 390 Journal
loc.l12.apiCopy API URL copy Copies the version URL with ?lang= LocaleFrame{s:{v:"sf",x:"apiCopy"}} 1440, 390 Journal
loc.l12.seoPreview SEO preview Localized address and canonical LocaleFrame{s:{v:"sf",x:"seoPreview"}} 1440, 390 Journal
loc.l12.siteFallback Site text fallback Used only when its language matches the article LocaleFrame{s:{v:"sf",x:"siteFallback"}} 1440, 390 Journal

Legacy IDs kept

Canonical ID Alias
loc.lang.disableDefault loc.l1.disableDefault
loc.lang.disableLast loc.l1.disableLast
loc.dup.readable loc.l4.dup
loc.dup.hidden loc.l4.dupHidden
loc.perm.readonly loc.l5b.ro
loc.perm.editNoPublish loc.l5b.p2
loc.perm.publishNoEdit loc.l5b.p3
loc.perm.siblingReadOnly loc.l5b.p4
loc.perm.siblingHidden loc.l5b.p5
loc.perm.createNotAll loc.l5b.p6
loc.lang.publishDisabled loc.l8.publishDisabled
loc.review.changed loc.l9.changed
loc.src.inaccessible loc.l9.inaccessible
loc.src.deleted loc.l9.gone
loc.seo.noFallback loc.l10.default
loc.slug.published loc.l10.editSlug
loc.url.unset loc.l10.unset
loc.conflict loc.l11.conflict

Changes against the previous 109

Added (70): L1:sl:broader, L1:sl:defaultFail, L1:sl:stale, L1:sl:reenableDone, L1:sl:reenableFail, L1:sl:removeFail, L1:sl:social, L1:sl:socialEdit, L1:sl:socialInvalid, L1b:pu:set, L1b:pu:editing, L1b:pu:unknown, L1b:pu:scheme, L1b:pu:encoded, L1b:pu:noSlug, L1b:pu:saved, L1b:pu:unset, L1b:pu:viewOnly, L2:su:noLocale, L2:su:inProgress, L4:at:unreadable, L4:at:langOff, L5:ed:cleanOnline, L5:ed:pubFail, L5:ed:ovf, L5:ed:titleEdit, L5:ed:titleUnsaved, L5:ed:titleShort, L5:ed:titleLong, L5b:perm:ro, L5b:perm:roMenu, L5b:perm:p2, L5b:perm:p3, L5b:perm:p4, L5b:perm:p5, L5b:perm:p6, L5b:perm:noAdd, L5b:perm:withAdd, L6:src:arRef, L7:li:page2, L7:li:review, L7:li:author, L7:li:langFilter, L7:li:rowMenu, L8:pd:publishDisabled, L8:pd:unpublishDirty, L8:pd:deleteStoryOk, L9:li:frReview, L10:seo:editSlug, L11:pm:restored, L11:pm:conflictRestored, L11:pm:reloadConfirm, L11:pm:conflict2, L11:pm:download, L11b:pm:moveSelect, L11b:pm:moveSelectHidden, L11b:pm:moveConfirm, L11b:pm:moveBusy, L11b:pm:moveDone, L11b:pm:move, L11b:pm:moveSlug, L11b:pm:movePerm, L12:sf:home, L12:sf:homeFr, L12:sf:activity, L12:sf:media, L12:sf:menu, L12:sf:apiCopy, L12:sf:seoPreview, L12:sf:siteFallback

Changed (rekeyed to a new group or frame, same meaning): L5b|ed|ro, L5b|ed|roMenu, L5b|ed|p2, L5b|ed|p3, L5b|ed|p4, L5b|ed|p5, L5b|ed|p6, L11|pm|move, L11|pm|moveSlug, L11|pm|movePerm

Removed without replacement: none.

Kept unchanged: 99.