feat: sites and authors integration into existing screens (& formated all frontend files kinda accidentally)
This commit is contained in:
1 parent
3d81d2c717
commit
6b8990c55e
239 files changed
+9194
-1354
No files matched your search
@@ -1907,6 +1907,96 @@ export const messages = {
|
||||
'{0} is now a guest author.': '{0} est maintenant un auteur invité.',
|
||||
'{0} will stay on their articles. New articles by {1} will no longer start with this author.':
|
||||
'{0} restera sur ses articles. Les nouveaux articles de {1} ne commenceront plus avec cet auteur.',
|
||||
'No authors. This article will have no byline.':
|
||||
'Aucun auteur. Cet article n’aura pas de signature.',
|
||||
'Reorder {0}': 'Réorganiser {0}',
|
||||
Primary: 'Principal',
|
||||
'Remove {0}': 'Retirer {0}',
|
||||
'Drag to reorder. The first author is the primary byline.':
|
||||
'Faites glisser pour réorganiser. Le premier auteur est la signature principale.',
|
||||
'Create author "{0}"…': 'Créer l’auteur « {0} »…',
|
||||
'Create author…': 'Créer un auteur…',
|
||||
'Search authors': 'Rechercher des auteurs',
|
||||
'Add an author': 'Ajouter un auteur',
|
||||
'No author matches "{0}".': 'Aucun auteur ne correspond à « {0} ».',
|
||||
"Couldn't create the author": 'Impossible de créer l’auteur',
|
||||
'The article is unchanged. Try again in a moment.':
|
||||
'L’article n’a pas changé. Réessayez dans un instant.',
|
||||
'Create author': 'Créer l’auteur',
|
||||
'Adds a byline for this article. You can add a bio and avatar later in Authors.':
|
||||
'Ajoute une signature pour cet article. Vous pourrez ajouter une bio et un avatar plus tard dans Auteurs.',
|
||||
'Generated from the name. Unique across all authors.':
|
||||
'Généré à partir du nom. Unique parmi tous les auteurs.',
|
||||
'This is me': 'C’est moi',
|
||||
'Links the author to {0}.': 'Lie l’auteur à {0}.',
|
||||
"You don't have an author profile yet": 'Vous n’avez pas encore de profil d’auteur',
|
||||
'Create one to be credited on your articles.':
|
||||
'Créez-en un pour être crédité sur vos articles.',
|
||||
'Create my author profile': 'Créer mon profil d’auteur',
|
||||
'Site and authors': 'Site et auteurs',
|
||||
'Canonical host:': 'Hôte canonique :',
|
||||
'Author profile created': 'Profil d’auteur créé',
|
||||
'{0} is now your byline.': '{0} est maintenant votre signature.',
|
||||
"Couldn't create your author profile": 'Impossible de créer votre profil d’auteur',
|
||||
'No authors': 'Aucun auteur',
|
||||
'{0} +{1}': '{0} +{1}',
|
||||
'{0} · {1}': '{0} · {1}',
|
||||
'{0} grabbed. Position {1} of {2}. Use the up and down arrow keys to move, Space to drop, Escape to cancel.':
|
||||
'{0} saisi. Position {1} sur {2}. Utilisez les flèches haut et bas pour déplacer, Espace pour déposer, Échap pour annuler.',
|
||||
'{0} moved to position {1} of {2}.': '{0} déplacé à la position {1} sur {2}.',
|
||||
'{0} dropped. Position {1} of {2}.': '{0} déposé. Position {1} sur {2}.',
|
||||
'Reorder cancelled. {0} is back at position {1} of {2}.':
|
||||
'Réorganisation annulée. {0} est revenu à la position {1} sur {2}.',
|
||||
'All sites': 'Tous les sites',
|
||||
'No articles on "{0}"': 'Aucun article sur « {0} »',
|
||||
'Move articles here from the editor, or choose another site.':
|
||||
'Déplacez des articles ici depuis l’éditeur, ou choisissez un autre site.',
|
||||
'Show all sites': 'Afficher tous les sites',
|
||||
'Author profile': 'Profil d’auteur',
|
||||
"Your public byline on articles. It's separate from your account.":
|
||||
'Votre signature publique sur les articles. Elle est distincte de votre compte.',
|
||||
'Uses your display name, {0}. You can change it afterwards.':
|
||||
'Utilise votre nom affiché, {0}. Vous pourrez le modifier ensuite.',
|
||||
'Uses your username, {0}. You can change it afterwards.':
|
||||
'Utilise votre nom d’utilisateur, {0}. Vous pourrez le modifier ensuite.',
|
||||
'Edit author': 'Modifier l’auteur',
|
||||
'Your slug is {0}': 'Votre slug est {0}',
|
||||
'{0} was already used by another author. You can change it in Edit author.':
|
||||
'{0} était déjà utilisé par un autre auteur. Vous pouvez le modifier dans Modifier l’auteur.',
|
||||
'Linked to your account. New articles start with this author.':
|
||||
'Lié à votre compte. Les nouveaux articles commencent avec cet auteur.',
|
||||
"You don't have an author profile yet. Create one to be credited on your own articles.":
|
||||
'Vous n’avez pas encore de profil d’auteur. Créez-en un pour être crédité sur vos propres articles.',
|
||||
'The sites an article can belong to. Any verified user can read them; changes are made in the browser by the owner.':
|
||||
'Les sites auxquels un article peut appartenir. Tout utilisateur vérifié peut les lire ; les modifications se font dans le navigateur par le propriétaire.',
|
||||
'List the sites, the default first and then by name, each with its article count.':
|
||||
'Liste les sites, le site par défaut d’abord puis par nom, chacun avec son nombre d’articles.',
|
||||
'You must be logged in to access this resource.':
|
||||
'Vous devez être connecté pour accéder à cette ressource.',
|
||||
'You must verify your account to access this resource.':
|
||||
'Vous devez vérifier votre compte pour accéder à cette ressource.',
|
||||
'The people credited on articles. Any verified user can read them; changes are made in the browser by an owner or admin.':
|
||||
'Les personnes créditées sur les articles. Tout utilisateur vérifié peut les lire ; les modifications se font dans le navigateur par un propriétaire ou un administrateur.',
|
||||
'List the authors by name, each with its article count.':
|
||||
'Liste les auteurs par nom, chacun avec son nombre d’articles.',
|
||||
'The page, size or search is not valid.':
|
||||
'La page, la taille ou la recherche n’est pas valide.',
|
||||
'Up to 200 characters, matched in the name or the slug.':
|
||||
'Jusqu’à 200 caractères, recherchés dans le nom ou le slug.',
|
||||
'Sites · owner only': 'Sites · propriétaire uniquement',
|
||||
'Create a site': 'Créer un site',
|
||||
'Edit a site': 'Modifier un site',
|
||||
'Make a site the default': 'Définir un site par défaut',
|
||||
'Delete a site, moving its articles with reassign_to':
|
||||
'Supprimer un site, en déplaçant ses articles avec reassign_to',
|
||||
'Authors · owner and admin': 'Auteurs · propriétaire et administrateur',
|
||||
'Create an author': 'Créer un auteur',
|
||||
'Edit an author, or your own author as a verified user':
|
||||
'Modifier un auteur, ou votre propre auteur en tant qu’utilisateur vérifié',
|
||||
'Delete an author, moving its articles with reassign_to':
|
||||
'Supprimer un auteur, en déplaçant ses articles avec reassign_to',
|
||||
'Create your own author profile, as any verified user':
|
||||
'Créer votre propre profil d’auteur, en tant qu’utilisateur vérifié',
|
||||
} as const
|
||||
|
||||
export type MessageKey = keyof typeof messages
|
||||
@@ -0,0 +1,126 @@
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { join } from 'node:path'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { API_GROUPS, SESSION_GROUPS } from '@/data/apiCatalogue'
|
||||
import { messages } from './messages'
|
||||
|
||||
const SOURCES = [
|
||||
'components/ArticleCardList.vue',
|
||||
'components/ArticleSiteFilter.vue',
|
||||
'components/ArticleTable.vue',
|
||||
'components/AuthorList.vue',
|
||||
'components/AuthorProfileCard.vue',
|
||||
'components/AuthorSelect.vue',
|
||||
'components/CreateAuthorDialog.vue',
|
||||
'components/CreateAuthorHint.vue',
|
||||
'components/EmptyState.vue',
|
||||
'components/MetaSection.vue',
|
||||
'components/SiteSelect.vue',
|
||||
'composables/useArticleActions.ts',
|
||||
'composables/useArticleRowActions.ts',
|
||||
'composables/useEditorImages.ts',
|
||||
'composables/useMyAuthorProfile.ts',
|
||||
'lib/articleMeta.ts',
|
||||
'views/ArticleView.vue',
|
||||
'views/EditorView.vue',
|
||||
'views/ProfileView.vue',
|
||||
]
|
||||
|
||||
const SAME_IN_FRENCH = new Set(['Articles', 'Site', 'Sites', '{0} +{1}', '{0} · {1}'])
|
||||
|
||||
const LITERAL = /\bt\(\s*(?:'((?:\\.|[^'\\])*)'|"((?:\\.|[^"\\])*)")/g
|
||||
|
||||
function keysIn(source: string) {
|
||||
return [...source.matchAll(LITERAL)].map((match) =>
|
||||
(match[1] ?? match[2] ?? '').replace(/\\u0022/g, '"').replace(/\\(['"\\])/g, '$1'),
|
||||
)
|
||||
}
|
||||
|
||||
function frenchOf(key: string) {
|
||||
return (messages as Record<string, string>)[key]
|
||||
}
|
||||
|
||||
describe('Sites and authors in the editor, articles and profile screens: messages', () => {
|
||||
const keys = SOURCES.flatMap((file) =>
|
||||
keysIn(readFileSync(join(import.meta.dirname, '..', file), 'utf8')).map((key) => ({
|
||||
file,
|
||||
key,
|
||||
})),
|
||||
)
|
||||
|
||||
it('finds the strings of the three screens', () => {
|
||||
expect(keys.length).toBeGreaterThan(40)
|
||||
})
|
||||
|
||||
it.each(keys.map(({ file, key }) => [file, key]))(
|
||||
'%s: "%s" has its own French entry',
|
||||
(_file, key) => {
|
||||
expect(frenchOf(key as string)).toMatch(/\S/)
|
||||
if (!SAME_IN_FRENCH.has(key as string)) {
|
||||
expect(frenchOf(key as string)).not.toBe(key)
|
||||
}
|
||||
},
|
||||
)
|
||||
})
|
||||
|
||||
describe('Catalogue groups of Sites and Authors in French', () => {
|
||||
const groups = API_GROUPS.filter((group) => ['sites', 'authors'].includes(group.key))
|
||||
const sessionGroups = SESSION_GROUPS.filter((group) => /^(Sites|Authors)/.test(group.title))
|
||||
|
||||
it('covers the four group titles of the feature', () => {
|
||||
expect([
|
||||
...groups.map((group) => group.title),
|
||||
...sessionGroups.map((group) => group.title),
|
||||
]).toEqual(['Sites', 'Authors', 'Sites · owner only', 'Authors · owner and admin'])
|
||||
})
|
||||
|
||||
it.each([
|
||||
...groups.flatMap((group) => [group.title, group.description]),
|
||||
...sessionGroups.map((group) => group.title),
|
||||
])('"%s" has a French entry', (text) => {
|
||||
expect(frenchOf(text)).toMatch(/\S/)
|
||||
})
|
||||
|
||||
it.each([
|
||||
...groups.flatMap((group) =>
|
||||
group.endpoints.flatMap((item) => [
|
||||
item.description,
|
||||
...item.errors.map((error) => error.caption),
|
||||
...item.query.map((field) => field.rule),
|
||||
]),
|
||||
),
|
||||
...sessionGroups.flatMap((group) => group.rows.map((row) => row.description)),
|
||||
])('"%s" has a French entry', (text) => {
|
||||
expect(frenchOf(text)).toMatch(/\S/)
|
||||
})
|
||||
})
|
||||
|
||||
describe('The owner_only copy of the design board', () => {
|
||||
it.each([
|
||||
"You don't have access to Sites",
|
||||
'Only the owner can manage sites. Ask the owner of this site if you need a change.',
|
||||
'Only the owner can manage sites',
|
||||
'Ask the owner of this site.',
|
||||
])('"%s" has a French entry', (text) => {
|
||||
expect(frenchOf(text)).toMatch(/\S/)
|
||||
expect(frenchOf(text)).not.toBe(text)
|
||||
})
|
||||
|
||||
it('is used by the 403 page and by the action toast', () => {
|
||||
const page = readFileSync(
|
||||
join(import.meta.dirname, '..', 'components/SitesForbidden.vue'),
|
||||
'utf8',
|
||||
)
|
||||
const toast = readFileSync(
|
||||
join(import.meta.dirname, '..', 'composables/useDirectoryToasts.ts'),
|
||||
'utf8',
|
||||
)
|
||||
|
||||
expect(page).toContain("You don\\'t have access to Sites")
|
||||
expect(page).toContain(
|
||||
'Only the owner can manage sites. Ask the owner of this site if you need a change.',
|
||||
)
|
||||
expect(toast).toContain('Only the owner can manage sites')
|
||||
expect(toast).toContain('Ask the owner of this site.')
|
||||
})
|
||||
})
|
||||
@@ -7,7 +7,11 @@ type ToastProvider = ToastSource | (() => ToastSource)
|
||||
const notifications = shallowReactive(
|
||||
new Map<
|
||||
string | number,
|
||||
{ source: () => ToastSource; toast: ReturnType<typeof useToast>; visible: boolean }
|
||||
{
|
||||
source: () => ToastSource
|
||||
toast: ReturnType<typeof useToast>
|
||||
visible: boolean
|
||||
}
|
||||
>(),
|
||||
)
|
||||
|
||||
|
||||
@@ -139,7 +139,11 @@ describe('Switch language without losing work', () => {
|
||||
total: items.length,
|
||||
page: 1,
|
||||
size: 25,
|
||||
counts: { all: items.length, unused: items.length, duplicates: 0 },
|
||||
counts: {
|
||||
all: items.length,
|
||||
unused: items.length,
|
||||
duplicates: 0,
|
||||
},
|
||||
}
|
||||
})
|
||||
await mountRoute('/media')
|
||||
@@ -378,9 +382,16 @@ describe('Hardener localization outcomes', () => {
|
||||
})
|
||||
it('copies the unchanged integration URL with French feedback and protocol examples', async () => {
|
||||
const writeText = vi.fn().mockResolvedValue(undefined)
|
||||
Object.defineProperty(navigator, 'clipboard', { value: { writeText }, configurable: true })
|
||||
Object.defineProperty(navigator, 'clipboard', {
|
||||
value: { writeText },
|
||||
configurable: true,
|
||||
})
|
||||
setLocale('fr')
|
||||
useSessionStore().session = { verified: true, username: 'marta', displayName: '' }
|
||||
useSessionStore().session = {
|
||||
verified: true,
|
||||
username: 'marta',
|
||||
displayName: '',
|
||||
}
|
||||
const { default: IntegrationView } = await import('@/views/IntegrationView.vue')
|
||||
wrapper = mount(IntegrationView, {
|
||||
attachTo: document.body,
|
||||
@@ -502,7 +513,9 @@ describe('Hardener localization outcomes', () => {
|
||||
}
|
||||
}
|
||||
vi.stubGlobal('XMLHttpRequest', LimitedUpload)
|
||||
vi.stubGlobal('crypto', { subtle: { digest: async () => new Uint8Array(32).buffer } })
|
||||
vi.stubGlobal('crypto', {
|
||||
subtle: { digest: async () => new Uint8Array(32).buffer },
|
||||
})
|
||||
request.mockResolvedValue({
|
||||
items: [
|
||||
{
|
||||
|
||||
Reference in new issue
Block a user