Feature: First-time language setup for existing articles Existing articles have no language. One owner confirms a default language for every site that has none, and all assignments apply in one transaction. It labels articles. It never translates, links or moves them. Background: Given the language rules of features/article_localization_storage.feature apply And error responses are plain text whose body is the snake_case code And the setup page is global: its route is /settings/sites/:key/languages/setup, where :key is only the site it was opened from And a site is "pending" when it has no confirmed default language And these sites exist | key | name | Locale | articles | state | | journal | Journal | en | 8 | unconfirmed default row "en" | | notes | Notes | null | 3 | no language row | | archive | Archive | pt-BR | 5 | unconfirmed default row "pt-BR" | | gallery | Gallery | en-GB | 0 | unconfirmed default row "en-GB" | Scenario: Read what is pending When an owner sends GET /v1/language/setup Then the status is 200 And "sites" lists every pending site with key, name, locale, article_count and a suggestion And the suggestion is { "lang": "pt-BR", "substituted": false } for archive and null for notes And "available" lists the selectable tags like GET /v1/site/{key}/languages And a site whose default is confirmed is not listed Scenario Outline: Read access When sends GET /v1/language/setup Then the status is Examples: | caller | status | | an anonymous client | 401 | | a reader | 403 | | an owner | 200 | Scenario: Apply for every pending site When an owner sends POST /v1/language/setup with | key | lang | | journal | en | | notes | en | | archive | pt-BR | | gallery | en-GB | Then the status is 200 And every listed site has a confirmed default language row with the chosen tag, enabled And all 16 articles carry the chosen tag of their site And the body says 16 articles were assigned, per site And every article keeps its id, title, body, slug, is_online, story_id and timestamps And no story is merged, split or moved And GET /v1/site/journal/languages has "needs_setup" false Scenario: The owner may change a suggestion When an owner confirms "archive" with "pt" instead of "pt-BR" Then archive has one language row "pt", confirmed and default And no row "pt-BR" remains And its 5 articles carry "pt" Scenario: A site with no articles still needs confirming When an owner posts every pending site except "gallery" Then the status is 422 and the body is "setup_incomplete" And nothing is applied Scenario Outline: Rows that are refused When an owner posts Then the status is and the body is "" And no site and no article has changed Examples: | body | status | code | | notes with lang "english" | 422 | invalid_language | | notes with lang "xx" | 422 | unsupported_language | | notes with an empty lang | 422 | invalid_language | | the same site key twice | 422 | duplicate_site | | a site key "nowhere" | 404 | site_not_found | | no "sites" key, or malformed JSON | 400 | Invalid request body | Scenario: Retrying is safe Given setup succeeded When the same POST is sent again Then the status is 200 and zero articles are assigned And no row, article or story changes And a request that asks for a different tag on an already confirmed site changes nothing for that site And the response lists that site key in "skipped" instead of dropping the choice silently Scenario: Failure leaves nothing half done Given the write fails while handling the second site When an owner posts every pending site Then the status is 500 And no site has a confirmed default and no article is assigned And a later POST completes every site Scenario: Another setup is running Given another owner's POST holds the setup When an owner posts Then the status is 409 and the body is "setup_in_progress" And nothing is applied When the first setup has finished and the owner posts again Then the status is 200 and the finished sites are skipped Scenario: A lock error anywhere in the setup is a conflict Given a site row, a read, a write or the commit of the setup fails on a database lock Then the status is 409 and the body is "setup_in_progress" and nothing is applied Scenario: A site that became pending after the first read is locked too Given a site is created after the setup read the pending sites and before it locked them Then the setup locks it as well before deciding, so it must be listed or the status is 422 "setup_incomplete" And no pending site is ever confirmed without its lock Scenario: Two simultaneous setups on SQLite Given a database on SQLite When two owners post the same body at the same time Then each answers 200 or 409 and never 500, at least one answers 200 And every article is assigned exactly once Scenario: Setup is recorded in the activity log When an owner completes the setup for several sites Then one "site.language_setup" event of the sites category is recorded per confirmed site after the commit, with the default_language change and the assigned count And a refused setup records one failed event with a plain reason, and skipped sites record nothing Scenario: Create racing setup never strands an article Given journal has unassigned articles When setup commits while articles are being created concurrently in journal Then every article of journal ends with the confirmed default tag And none is left unassigned and none has another tag Scenario: Locks are taken in a fixed order Given two owners post the same pending sites in different row orders Then neither request deadlocks and both finish with 200 or one with 409 "setup_in_progress" Scenario: Persistence portability of the setup transaction Then one targeted task test:mysql and task test:postgres run covers the setup transaction and its lock order Scenario: Sites created after the migration stay pending Given a site is created after slice 1 and has no language row Then it is listed as pending by GET /v1/language/setup And seeding a suggested row on site create is out of scope and tracked as a follow-up Scenario: Route and access Given the route /settings/sites/:key/languages/setup Then it is open to owners only (settings.manage_sites) And the breadcrumb and back link go to the Languages page of :key And it is reached from the "Set up languages" banner of a site with needs_setup true Scenario Outline: Setup states When Then the screen is "" () and shows Examples: | trigger | state | id | visible | | the owner opens setup with Journal, Notes and Archive pending | Ready to assign | loc.l2.ready | heading "Set article languages", subtitle "Existing articles don't have a language yet. Confirm or change the language for each site.", the info alert "This labels articles. It doesn't translate them." / "Each existing article becomes the only version of its own article group. Each site's language starts from its locale with the region kept: pt-BR stays Portuguese (Brazil). Nothing is applied until you confirm."; rows "Journal 8 articles · Site locale en" with "English · en", "Notes 3 articles · No site locale" with "Choose a language" and the error "Required before you can apply.", "Archive 5 articles · Site locale pt-BR" with "Portuguese (Brazil) · pt-BR"; helper "Seeded from site locale en. Confirm or change.". The button "Confirm and assign to 16 articles" is disabled until every row has a language; the note "Counts are articles in 3 sites." | | only Notes is pending | No-locale site | loc.l2.noLocale | the error alert "Choose a language for Notes" / "Notes has no site locale, so there is nothing to start from. Choose the language its articles are written in." and the confirm button disabled | | only Gallery is pending (0 articles, en-GB) | Site with no articles | loc.l2.empty | the page "Languages" / "Content languages for Gallery." with the card "Confirm the default language", "Gallery has no articles yet. New articles will be written in this language.", the field "Default content language" "English (United Kingdom) · en-GB" with "Seeded from site locale en-GB. The full locale is kept. Confirm or change it.", and a "Save" button | | the owner confirms and the POST is in flight | Applying | loc.l2.applying | the info alert "Assigning languages" / "Articles created while this runs get the site's default language." and the busy button "Assigning…" disabled | | the POST returns 409 setup_in_progress | Setup already running | loc.l2.inProgress | the info alert "Setup is already running" / "Another owner started it. Languages appear here when it finishes. Articles created meanwhile get each site's default language. Starting it again is safe: sites that already have a language are skipped." with a "Check again" button that repeats GET /v1/language/setup; the rows are read-only and the confirm button disabled | | the POST returns 200 with 16 articles assigned | Done | loc.l2.done | the success alert "Languages assigned" / "16 articles now have a language. Nothing was translated, and no articles were grouped." with an "Open Articles" button; the confirm button is gone | | the POST fails with a server error | Failed | loc.l2.fail | the error alert "Nothing was changed" / "The update failed, so no article has a language yet. Your choices are kept. Try again. Running it twice is safe." with a "Try again" button; the rows keep their values | Scenario: The page sends one request for all rows When the owner confirms with Journal "en", Notes "en", Archive "pt-BR" Then exactly one POST /v1/language/setup is sent with all three rows And the Languages page of every listed site no longer shows the setup banner Scenario: Nothing is pending Given the owner opens the setup page and no site is pending, including after "Check again" resolves Then the Done success alert "Languages assigned" is shown without an article count And this state has no frame, so the assumption is recorded in the QA notes Scenario: Unknown or already complete :key Given the page is opened with a :key that is unknown or whose default is already confirmed Then the page still lists the globally pending sites And the back link and breadcrumb go to the Languages page of :key, or to Settings, Sites when the :key is unknown And this case has no frame, so the assumption is recorded in the QA notes Scenario: Substituted seed Given a site's suggestion has "substituted" true (Locale "fr-BE" gives "fr") Then the row shows the seeded tag "fr" And its help text reuses the Languages "broader" wording, the closest drawn copy And this state has no frame, so the assumption is recorded in the QA notes