feat: editorial article list

This commit is contained in:
Florian Sylvain committed 2026-11-02 07:28:44 +01:00
1 parent a0b3c58c52
commit a671b81f80
103 files changed
+6493 -1805

No files matched your search

+77
View File
@@ -0,0 +1,77 @@
import { readFileSync } from 'node:fs'
import { join } from 'node:path'
import { describe, expect, it } from 'vitest'
import { articleListMessages } from './articleListMessages'
import { messages } from './messages'
const SOURCES = [
'components/ArticleFilterBar.vue',
'components/ArticleFilterChips.vue',
'components/ArticleFilterFields.vue',
'components/ArticleFiltersSheet.vue',
'components/ArticleListEmpty.vue',
'components/ArticleListFailure.vue',
'components/ArticleListTabs.vue',
'components/StoryCardList.vue',
'components/StoryCoverage.vue',
'components/StoryRowMenu.vue',
'components/StoryTable.vue',
'lib/articleFilterChips.ts',
'lib/storyList.ts',
'lib/storyMenu.ts',
'views/ArticleView.vue',
]
const SAME_IN_FRENCH = new Set([
'SEO',
'Site',
'Article',
'Articles',
'Actions',
'1 version',
'{0} versions',
'1 article',
'{0} articles',
])
const LITERAL = /\bt\(\s*(?:'((?:\\.|[^'\\])*)'|"((?:\\.|[^"\\])*)")/g
const NUMBERED = /\{\d+\}/g
function keysIn(source: string) {
return [...source.matchAll(LITERAL)].map((match) =>
(match[1] ?? match[2] ?? '').replace(/\\(['"\\])/g, '$1'),
)
}
describe('Article list messages', () => {
const keys = SOURCES.flatMap((file) =>
keysIn(readFileSync(join(import.meta.dirname, '..', file), 'utf8')).map((key) => ({
file,
key,
})),
)
it('finds the strings of the list screen', () => {
expect(keys.length).toBeGreaterThan(60)
})
it.each(keys.map(({ file, key }) => [file, key]))(
'%s: "%s" has a French entry',
(_file, key) => {
const french = (messages as Record<string, string>)[key as string]
expect(french).toMatch(/\S/)
if (!SAME_IN_FRENCH.has(key as string)) {
expect(french).not.toBe(key)
}
},
)
it.each(Object.entries(articleListMessages))(
'keeps the placeholders of "%s" in French',
(english, french) => {
expect([...french.matchAll(NUMBERED)].map((m) => m[0]).sort()).toEqual(
[...english.matchAll(NUMBERED)].map((m) => m[0]).sort(),
)
},
)
})
+80
View File
@@ -0,0 +1,80 @@
export const articleListMessages = {
'Needs translation': 'À traduire',
All: 'Tous',
Published: 'Publiés',
Drafts: 'Brouillons',
'Not in {0}': 'Pas en {0}',
'Article views': 'Vues des articles',
'Scroll tabs left': 'Faire défiler les onglets vers la gauche',
'Scroll tabs right': 'Faire défiler les onglets vers la droite',
'{0} {1} versions': '{0} versions en {1}',
'1 {0} version': '1 version en {0}',
'{0} {1} drafts': '{0} brouillons en {1}',
'1 {0} draft': '1 brouillon en {0}',
'{0} published {1} versions': '{0} versions publiées en {1}',
'1 published {0} version': '1 version publiée en {0}',
'{0} articles not in {1}': '{0} articles sans version en {1}',
'1 article not in {0}': '1 article sans version en {0}',
'{0} articles need translation': '{0} articles à traduire',
'1 article needs translation': '1 article à traduire',
'Search titles in every language': 'Rechercher dans les titres de toutes les langues',
'Search {0} titles': 'Rechercher dans les titres en {0}',
'All languages': 'Toutes les langues',
'Any status': 'Tout statut',
'Any author': 'Tout auteur',
'Any SEO state': 'Tout état SEO',
'Filters · {0}': 'Filtres · {0}',
'Filters, {0} active': 'Filtres, {0} actifs',
'Close filters': 'Fermer les filtres',
'Site: {0}': 'Site : {0}',
'Status: {0}': 'Statut : {0}',
'Author: {0}': 'Auteur : {0}',
'SEO: Needs attention': 'SEO : À examiner',
'Disabled languages keep their versions. They can’t get new ones.':
'Les langues désactivées gardent leurs versions. Elles n’en reçoivent pas de nouvelles.',
'{0} versions': '{0} versions',
'1 version': '1 version',
Authors: 'Auteurs',
SEO: 'SEO',
Actions: 'Actions',
'No articles match these filters': 'Aucun article ne correspond à ces filtres',
'Remove a filter to see more articles.': 'Retirez un filtre pour voir plus d’articles.',
'Shown in {0}': 'Affiché en {0}',
'Shown in {0} · language disabled': 'Affiché en {0} · langue désactivée',
'Opens {0} · published': 'Ouvre {0} · publié',
'Opens {0} · draft': 'Ouvre {0} · brouillon',
'Opens {0} · published · language disabled': 'Ouvre {0} · publié · langue désactivée',
'Opens {0} · draft · language disabled': 'Ouvre {0} · brouillon · langue désactivée',
'Add {0} translation': 'Ajouter une traduction en {0}',
'Add {0} translation to {1}': 'Ajouter une traduction en {0} à {1}',
'Open {0}': 'Ouvrir {0}',
'Open another language…': 'Ouvrir une autre langue…',
'Add translation…': 'Ajouter une traduction…',
'Publish {0}': 'Publier {0}',
'Unpublish {0}': 'Dépublier {0}',
'Delete {0} version': 'Supprimer la version {0}',
'Open this version': 'Ouvrir cette version',
published: 'publié',
draft: 'brouillon',
'no version': 'aucune version',
'language disabled': 'langue désactivée',
'{0}: {1}': '{0} : {1}',
'{0} more languages: {1}': '{0} autres langues : {1}',
'1 more language: {0}': '1 autre langue : {0}',
'Every article you can see has a {0} version':
'Tous les articles que vous voyez ont une version en {0}',
'Articles without a {0} version will appear here.':
'Les articles sans version en {0} apparaîtront ici.',
'Every article you can see is fully translated':
'Tous les articles que vous voyez sont entièrement traduits',
'Articles that need a translation will appear here.':
'Les articles à traduire apparaîtront ici.',
'Search covers titles in every language you can read.':
'La recherche porte sur les titres de toutes les langues que vous pouvez lire.',
"Couldn't load articles": 'Impossible de charger les articles',
'Your filters and page are kept. Try again.':
'Vos filtres et votre page sont conservés. Réessayez.',
'Showing only what you can read': 'Seul ce que vous pouvez lire s’affiche',
"Languages, counts and missing-translation results reflect the versions you have permission to see. You can't create translations on this site.":
'Les langues, les totaux et les traductions manquantes reflètent les versions que vous avez le droit de voir. Vous ne pouvez pas créer de traductions sur ce site.',
}
+2
View File
@@ -1,7 +1,9 @@
import { articleListMessages } from './articleListMessages'
import { localizationMessages } from './localizationMessages'
export const messages = {
...localizationMessages,
...articleListMessages,
'Not found': 'Introuvable',
'Edit article': 'Modifier l’article',
unselected: 'désélectionnée',
-4
View File
@@ -5,11 +5,7 @@ import { API_GROUPS, SESSION_GROUPS } from '@/data/apiCatalogue'
import { messages } from './messages'
const SOURCES = [
'components/ArticleCardList.vue',
'components/ArticleListNotice.vue',
'components/ArticleListToolbar.vue',
'components/ArticleSiteFilter.vue',
'components/ArticleTable.vue',
'components/AuthorList.vue',
'components/AuthorProfileCard.vue',
'components/AuthorSelect.vue',
-3
View File
@@ -10,20 +10,17 @@ import type { ResolvedSeo, SeoSuggestion, SuggestionCode } from '@/lib/seoTypes'
const SOURCES = [
'components/ApiPreviewDialog.vue',
'components/ArticleListNotice.vue',
'components/ArticleListToolbar.vue',
'components/EditorHeader.vue',
'components/EditorMobileBar.vue',
'components/EditorSkeleton.vue',
'components/EmptyState.vue',
'components/FallbackChip.vue',
'components/NeedsAttentionToggle.vue',
'components/SearchPreview.vue',
'components/SearchSharingSection.vue',
'components/SeoAdvancedSection.vue',
'components/SeoDisclosure.vue',
'components/SeoPanel.vue',
'components/SeoSocialSection.vue',
'components/SeoStatusChip.vue',
'components/SocialCardPreview.vue',
'components/SoftCounter.vue',
'components/SuggestionList.vue',
+15 -5
View File
@@ -13,7 +13,7 @@ import { initializeLocale, setLocale, t } from './index'
import { messages } from './messages'
import { formatAbsoluteDateFromIso } from '@/lib/relativeDate'
import { formatSize, pluralArticles } from '@/lib/mediaFormat'
import { articleEnvelope, articleListUrl } from '@/views/__tests__/articleEnvelope'
import { storyEnvelope, storyListUrl } from '@/views/__tests__/articleEnvelope'
import { accessFor, ownerAccess } from '@/test/access'
vi.mock('@/composables/useApi', async () => {
@@ -371,9 +371,13 @@ describe('Hardener localization outcomes', () => {
updated_at: '2025-02-14T12:00:00Z',
}
request.mockImplementation(async (url: string) =>
articleEnvelope([article], {
q: new URL(url, 'http://localhost').searchParams.get('q') ?? '',
}),
url.startsWith('/v1/article/')
? storyEnvelope([article], {
q: new URL(url, 'http://localhost').searchParams.get('q') ?? '',
})
: url.startsWith('/v1/author/')
? { items: [], total: 0, page: 1, size: 100 }
: [],
)
setLocale('fr')
await mountRoute('/article?q=absent')
@@ -438,7 +442,13 @@ describe('Hardener localization outcomes', () => {
updated_at: '2025-02-14T12:00:00Z',
}
request.mockImplementation(async (url: string) =>
url === articleListUrl() ? articleEnvelope([article]) : article,
url === storyListUrl()
? storyEnvelope([article])
: url.startsWith('/v1/site/') || url.startsWith('/v1/author/')
? url.startsWith('/v1/author/')
? { items: [], total: 0, page: 1, size: 100 }
: []
: article,
)
await mountRoute(path)
if (path === '/article') {