feat: relation field and picker

This commit is contained in:
Florian Sylvain committed 2026-10-23 00:13:42 +02:00
1 parent b45c99be3b
commit 37a77c9c22
92 files changed
+6826 -80

No files matched your search

+1 -1
View File
@@ -16,7 +16,7 @@ const SOURCES = [
'lib/schemaBuilderDiff.ts',
]
const SAME_IN_FRENCH = new Set<string>(['Date'])
const SAME_IN_FRENCH = new Set<string>(['Date', 'Relation'])
const LITERAL = /\bt\(\s*(?:'((?:\\.|[^'\\])*)'|"((?:\\.|[^"\\])*)")/g
+55
View File
@@ -2663,6 +2663,61 @@ export const messages = {
"A media field can't have a default, so the {0} existing entries would have no image. Make {1} optional to add it.":
'Un champ média ne peut pas avoir de valeur par défaut, donc les {0} entrées existantes n’auraient pas d’image. Rendez {1} facultatif pour l’ajouter.',
'{0} entries are unaffected.': '{0} entrées ne sont pas concernées.',
'Entry no longer exists': 'Cette entrée n’existe plus',
'Choose a {0}.': 'Choisissez une valeur pour {0}.',
'{0} must point to an existing entry.': '{0} doit pointer vers une entrée existante.',
'That {0} is already used by another entry.':
'Cette valeur de {0} est déjà utilisée par une autre entrée.',
Relation: 'Relation',
"Links to another type's entries": 'Renvoie vers les entrées d’un autre type',
"A relation can't have a default, so the existing entry would have no {0}. Make {0} optional to add it.":
'Une relation ne peut pas avoir de valeur par défaut, donc l’entrée existante n’aurait pas de valeur pour {0}. Rendez {0} facultatif pour l’ajouter.',
"A relation can't have a default, so the {0} existing entries would have no {1}. Make {1} optional to add it.":
'Une relation ne peut pas avoir de valeur par défaut, donc les {0} entrées existantes n’auraient pas de valeur pour {1}. Rendez {1} facultatif pour l’ajouter.',
Entries: 'Entrées',
'Each {0} has one {1}, and each {1} has one {0}.':
'Chaque {0} a un seul {1}, et chaque {1} a un seul {0}.',
'Each {0} has one {1}. A {1} has many {2}.':
'Chaque {0} a un seul {1}. Un {1} a plusieurs {2}.',
'Each {0} has many {1}, and each {2} has many {3}.':
'Chaque {0} a plusieurs {1}, et chaque {2} a plusieurs {3}.',
'Choose a related content type.': 'Choisissez un type de contenu lié.',
'{0} has a field that points to {1}. Remove that field first.':
'{0} a un champ qui pointe vers {1}. Supprimez d’abord ce champ.',
'Another content type has a field that points to {0}. Remove that field first.':
'Un autre type de contenu a un champ qui pointe vers {0}. Supprimez d’abord ce champ.',
'{0} is required. Choose another to save.':
'{0} est obligatoire. Choisissez une autre valeur pour enregistrer.',
'An {0} can have many {1}.': 'Un {0} peut avoir plusieurs {1}.',
'A {0} can have many {1}.': 'Un {0} peut avoir plusieurs {1}.',
'Remove {0} from {1}': 'Retirer {0} de {1}',
'{0} set to {1}.': '{0} défini sur {1}.',
'{0} removed from {1}.': '{0} retiré de {1}.',
'{0} added to {1}.': '{0} ajouté à {1}.',
'{0} results': '{0} résultats',
'No results': 'Aucun résultat',
'Search to add': 'Rechercher pour ajouter',
'Searching…': 'Recherche en cours…',
"Couldn't load {0}": 'Impossible de charger {0}',
'You can add up to {0}.': 'Vous pouvez en ajouter jusqu’à {0}.',
'No {0} yet. Add one in Content, then come back.':
'Aucun élément dans {0} pour l’instant. Ajoutez-en depuis Contenu, puis revenez.',
'No {0} match "{1}"': 'Aucun résultat dans {0} pour « {1} »',
'No other {0} to add.': 'Aucun autre élément de {0} à ajouter.',
one: 'un',
many: 'plusieurs',
'One to one': 'Un à un',
'One to many': 'Un à plusieurs',
'Many to many': 'Plusieurs à plusieurs',
"The related type and the connection can't change after saving.":
'Le type lié et la connexion ne peuvent plus changer après l’enregistrement.',
'Related content type': 'Type de contenu lié',
'Choose the content type this field points to.':
'Choisissez le type de contenu vers lequel ce champ pointe.',
'Choose a content type': 'Choisissez un type de contenu',
'How are they connected?': 'Comment sont-ils liés ?',
'Each {0} has': 'Chaque {0} a',
'shown on {0} as the field "{1}"': 'affiché sur {0} comme le champ « {1} »',
} as const
export type MessageKey = keyof typeof messages
@@ -0,0 +1,53 @@
import { readFileSync } from 'node:fs'
import { join } from 'node:path'
import { describe, expect, it } from 'vitest'
import { messages } from './messages'
const SOURCES = [
'components/RelationField.vue',
'components/RelationPicker.vue',
'components/DeleteTypeDialog.vue',
'lib/relationSentences.ts',
'lib/fieldTypes.ts',
'lib/entryCells.ts',
'lib/entryMessages.ts',
'lib/schemaBuilderValidation.ts',
]
const SAME_IN_FRENCH = new Set<string>(['Relation'])
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'),
)
}
describe('Relation field messages', () => {
const keys = SOURCES.flatMap((file) =>
keysIn(readFileSync(join(import.meta.dirname, '..', file), 'utf8')).map((key) => ({
file,
key,
})),
)
it('finds the strings of the feature', () => {
expect(keys.length).toBeGreaterThan(40)
})
it.each(keys.map(({ file, key }) => [file, key]))(
'%s: "%s" has its own 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) &&
!(key as string).match(/^(Date|Text|Number)$/)
) {
expect(french).not.toBe(key)
}
},
)
})