diff --git a/features/spa_internationalization.feature b/features/spa_internationalization.feature new file mode 100644 index 0000000..f82add2 --- /dev/null +++ b/features/spa_internationalization.feature @@ -0,0 +1,79 @@ +Feature: English and French throughout the SPA + + Background: + Given all existing SPA interface text supports English and French + And English preserves existing copy and French provides equivalent meaning + And user content, API data and transactional emails remain unchanged + And visual checks are waived by the user for this slice + And the language control uses existing design tokens + + Scenario Outline: Resolve the language before rendering + Given the browser-local stored language is + When the SPA starts + Then the interface language and document lang are + And browser language does not override the choice + + Examples: + | stored | locale | + | absent | en | + | en | en | + | fr | fr | + | unsupported | en | + | unreadable | en | + + Scenario Outline: Open the language picker in the profile submenu + Given the user opens the + When they activate the localized Language control + Then a compact scrollable picker offers English and Français + And the selected language is visibly and accessibly identified + And the picker uses Nuxt UI and existing design tokens + + Examples: + | menu | + | desktop UserMenu | + | mobile AccountMenuSheet | + + Scenario: Switch language without losing work + Given the user is editing an unsaved display name on /profile + When they select Français + Then all SPA interface text immediately uses French + And the document lang is fr + And the route, session, theme and unsaved work remain unchanged + And no reload, navigation or API request is caused by switching + When they select English + Then the interface immediately restores English with the same preserved state + + Scenario: Remember language on this browser + Given the user selected Français + When they navigate, reload, log out or return on this browser + Then French remains selected including on public authentication screens + And no language preference is sent to the server + + Scenario: Storage failure does not prevent switching + Given preference storage cannot be written + When the user selects Français + Then French is used for the current session without an error + And a fresh load without a readable preference defaults to English + + Scenario: Translate the entire interface + Given French is selected + When an existing SPA screen or shared component renders + Then navigation, forms, validation, notifications, dialogs and accessibility labels use French + And counts, human-readable dates and numbers use the selected locale + And Nuxt UI controls use the selected locale + And English and French catalogs have matching keys and interpolation parameters + And an unavailable French message falls back to English + And user content, account details, filenames, URLs, API data and emails remain unchanged + + Scenario Outline: Operate the picker accessibly + Given the picker is open in the + When the user navigates its options with the keyboard and confirms a choice + Then the language changes and the picker closes + And focus returns to its language control + When they reopen it and press Escape or click outside + Then the picker closes without changing language + + Examples: + | menu | + | desktop UserMenu | + | mobile AccountMenuSheet | diff --git a/features/spa_internationalization.qa.md b/features/spa_internationalization.qa.md new file mode 100644 index 0000000..3721c3f --- /dev/null +++ b/features/spa_internationalization.qa.md @@ -0,0 +1,21 @@ +# SPA internationalization QA + +Accepted by the user on 2026-10-03 together with the slice specification. + +## Automated + +1. Verify absent, English, French, unsupported and unreadable stored preferences; English defaults independently of browser language and document lang follows selection. +2. Verify desktop UserMenu and mobile AccountMenuSheet offer a localized language entry and a compact bounded scrollable picker with English and Français, selected-state semantics, selection, dismissal and focus return. Preserve profile, theme and logout behavior. +3. Switch both ways while editing an unsaved profile display name. Verify translated UI and unchanged draft, route, session and theme without additional API requests. +4. Verify browser-local persistence across reload initialization, navigation and logout/public authentication. Verify storage write failure allows switching for the current session. +5. Preserve existing English tests and add French checks for login, validation, profile save failure, integration copy feedback, not-found, representative dialogs and notifications. +6. Audit every existing view, shared component, composable and library producing interface text, including editor, media, users, invitations, settings and updates. Verify Nuxt UI controls use the active locale. +7. Assert catalog key/interpolation parity, nonempty translations, pluralization, English fallback and locale-sensitive formatting. Use fixed input dates without freezing system time. Verify displayed alerts and dialogs react to language changes. +8. Verify user content, account details, filenames, URLs, API fields, integration protocol examples, server release notes and transactional emails remain unchanged. +9. Run task test:web, frontend type checks, applicable lint and final build checks. Inspect introduced comments and ensure application changes are SPA-only. + +## Visual and manual + +Visual checks are SKIPPED by explicit user waiver for this slice because the designer is unavailable. The user authorized a coherent language control using existing design tokens. Do not edit design sources or claim pixel parity. + +Manual checklist: desktop/mobile and light/dark coherence, viewport containment, constrained scrolling, French text readability, touch and mouse selection, keyboard movement and confirmation, Escape/outside dismissal, focus return and visible focus. Confirm language retention after reload and logout. Do not freeze dates or investigate clock-sensitive states. diff --git a/web/src/App.vue b/web/src/App.vue index 520648a..cbd1e95 100644 --- a/web/src/App.vue +++ b/web/src/App.vue @@ -1,5 +1,7 @@ diff --git a/web/src/components/AccountMenuSheet.vue b/web/src/components/AccountMenuSheet.vue index 67b5248..304f140 100644 --- a/web/src/components/AccountMenuSheet.vue +++ b/web/src/components/AccountMenuSheet.vue @@ -1,9 +1,12 @@ + diff --git a/web/src/components/MediaEmptyState.vue b/web/src/components/MediaEmptyState.vue index 351fa57..72bb03d 100644 --- a/web/src/components/MediaEmptyState.vue +++ b/web/src/components/MediaEmptyState.vue @@ -1,4 +1,6 @@ diff --git a/web/src/components/MediaGrid.vue b/web/src/components/MediaGrid.vue index 888c857..e8727e4 100644 --- a/web/src/components/MediaGrid.vue +++ b/web/src/components/MediaGrid.vue @@ -1,4 +1,6 @@ diff --git a/web/src/components/ServerCommand.vue b/web/src/components/ServerCommand.vue index 99e213e..a003413 100644 --- a/web/src/components/ServerCommand.vue +++ b/web/src/components/ServerCommand.vue @@ -1,4 +1,6 @@ + diff --git a/web/src/components/UpdatesCard.vue b/web/src/components/UpdatesCard.vue index 4be1f34..b0cf8a9 100644 --- a/web/src/components/UpdatesCard.vue +++ b/web/src/components/UpdatesCard.vue @@ -1,4 +1,6 @@ @@ -207,9 +228,11 @@ onMounted(loadArticles)
-

Recently updated

+

+ {{ t('Recently updated') }} +

- All articles + {{ t('All articles') }}
- New article + {{ t('New article') }} @@ -279,8 +304,8 @@ onMounted(loadArticles) orientation="vertical" :close="false" icon="i-lucide-circle-x" - title="Couldn't load your articles" - description="Check your connection and try again." + :title="t('Couldn\'t load your articles')" + :description="t('Check your connection and try again.')" :ui="{ actions: 'mt-2' }" > @@ -304,16 +329,21 @@ onMounted(loadArticles)
-

Connect your website

+

+ {{ t('Connect your website') }} +

- Your site reads published articles from RenewCMS. Share this guide with - whoever builds it. + {{ + t( + 'Your site reads published articles from RenewCMS. Share this guide with whoever builds it.', + ) + }}

- Open the guide + {{ t('Open the guide') }}
diff --git a/web/src/views/IntegrationView.vue b/web/src/views/IntegrationView.vue index 01fe13e..d2faa30 100644 --- a/web/src/views/IntegrationView.vue +++ b/web/src/views/IntegrationView.vue @@ -1,4 +1,6 @@