feat: editorial article list
This commit is contained in:
1 parent
a0b3c58c52
commit
a671b81f80
103 files changed
+6493
-1805
No files matched your search
@@ -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(),
|
||||
)
|
||||
},
|
||||
)
|
||||
})
|
||||
@@ -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.',
|
||||
}
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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') {
|
||||
|
||||
Reference in new issue
Block a user