From 721311a6433a3c7dd0f7b71dfc5dec25aa7e5a12 Mon Sep 17 00:00:00 2001 From: Florian Sylvain Date: Sat, 3 Oct 2026 01:53:49 +0200 Subject: [PATCH] feat(SPA): i18n internationzalization, english and french --- features/spa_internationalization.feature | 79 ++ features/spa_internationalization.qa.md | 21 + web/src/App.vue | 4 +- web/src/components/AccountMenuSheet.vue | 24 +- web/src/components/AppBoot.vue | 6 +- web/src/components/ArticleCardList.vue | 23 +- web/src/components/ArticleDetails.vue | 81 +- web/src/components/ArticleEditor.vue | 156 +++- web/src/components/ArticleEditorImageNode.vue | 19 +- web/src/components/ArticleStatusBadge.vue | 8 +- web/src/components/ArticleTable.vue | 69 +- web/src/components/AuthShell.vue | 4 +- web/src/components/BulkDeleteMediaDialog.vue | 36 +- web/src/components/CharacterCounter.vue | 19 +- web/src/components/CodeBlock.vue | 6 +- web/src/components/ConfirmDialog.vue | 13 +- web/src/components/DashboardShell.vue | 61 +- web/src/components/DeleteMediaDialog.vue | 20 +- web/src/components/DuplicateGroups.vue | 21 +- web/src/components/EmptyState.vue | 42 +- web/src/components/EndpointCard.vue | 9 +- web/src/components/ErrorView.vue | 44 +- web/src/components/ImageUploader.vue | 127 ++- web/src/components/InviteStatusBadge.vue | 14 +- web/src/components/InviteUserModal.vue | 32 +- web/src/components/LanguagePicker.vue | 44 + .../components/MediaArticleStatusBadge.vue | 4 +- web/src/components/MediaBulkBar.vue | 10 +- web/src/components/MediaCardBadges.vue | 8 +- web/src/components/MediaCardList.vue | 4 +- web/src/components/MediaDetailPanel.vue | 73 +- web/src/components/MediaDropArea.vue | 5 +- web/src/components/MediaEmptyState.vue | 38 +- web/src/components/MediaGrid.vue | 4 +- web/src/components/MediaPicker.vue | 45 +- web/src/components/MediaSelectAllRow.vue | 6 +- web/src/components/MediaSkeletonGrid.vue | 4 +- web/src/components/MediaTable.vue | 24 +- web/src/components/MediaToolbar.vue | 77 +- web/src/components/MediaUploadPanel.vue | 29 +- web/src/components/MergeMediaDialog.vue | 27 +- web/src/components/NewArticleModal.vue | 50 +- web/src/components/OfflineBanner.vue | 4 +- web/src/components/PasswordStrengthHint.vue | 3 +- web/src/components/PublishControl.vue | 8 +- web/src/components/ReleaseNotes.vue | 4 +- web/src/components/RenameMediaDialog.vue | 29 +- web/src/components/RestartOverlay.vue | 36 +- web/src/components/RoleBadge.vue | 7 +- web/src/components/SaveStatus.vue | 24 +- web/src/components/ServerCommand.vue | 14 +- web/src/components/UpdateBanner.vue | 27 +- web/src/components/UpdatePill.vue | 11 +- web/src/components/UpdatesCard.vue | 212 +++-- web/src/components/UserCardList.vue | 4 +- web/src/components/UserEmailCell.vue | 8 +- web/src/components/UserMenu.vue | 44 +- web/src/components/UserRowAction.vue | 12 +- web/src/components/UsersTable.vue | 14 +- web/src/composables/useAccountMenu.ts | 22 +- web/src/i18n/i18n.spec.ts | 54 ++ web/src/i18n/index.ts | 51 ++ web/src/i18n/messages.ts | 827 ++++++++++++++++++ web/src/i18n/presentation.ts | 67 ++ web/src/i18n/spa.spec.ts | 769 ++++++++++++++++ web/src/lib/imageUpload.ts | 22 +- web/src/lib/mediaDuplicates.ts | 16 +- web/src/lib/mediaFormat.ts | 29 +- web/src/lib/mediaUploadRows.ts | 52 +- web/src/lib/registrationToasts.ts | 9 +- web/src/lib/relativeDate.ts | 18 +- web/src/lib/updateFormat.ts | 23 +- web/src/views/AcceptInviteView.vue | 116 ++- web/src/views/ArticleView.vue | 160 +++- web/src/views/EditorView.vue | 196 +++-- web/src/views/HomeView.vue | 88 +- web/src/views/IntegrationView.vue | 69 +- web/src/views/LoginView.vue | 117 +-- web/src/views/MediaView.vue | 189 ++-- web/src/views/NotFoundView.vue | 8 +- web/src/views/PendingView.vue | 80 +- web/src/views/ProfileView.vue | 132 ++- web/src/views/RegisterView.vue | 96 +- web/src/views/ResetConfirmView.vue | 88 +- web/src/views/ResetRequestView.vue | 50 +- web/src/views/SettingsView.vue | 137 ++- web/src/views/UsersView.vue | 105 ++- web/src/views/VerifyView.vue | 145 +-- 88 files changed, 4438 insertions(+), 1148 deletions(-) create mode 100644 features/spa_internationalization.feature create mode 100644 features/spa_internationalization.qa.md create mode 100644 web/src/components/LanguagePicker.vue create mode 100644 web/src/i18n/i18n.spec.ts create mode 100644 web/src/i18n/index.ts create mode 100644 web/src/i18n/messages.ts create mode 100644 web/src/i18n/presentation.ts create mode 100644 web/src/i18n/spa.spec.ts 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 @@