v3 : neuvième relecture — noms uniques, places justes, quantités
- Noms des liens d'équipe : comparés sans casse, accents ni ponctuation (ld_engine::names), lettres latines seulement (pas de sosie cyrillique, grec ou pleine chasse), une ligne ; index unique (migration additive 0003) contre deux ouvertures simultanées sous le même nom. - Une personne déjà connectée ne prend une place que si le lien lui ajoute un groupe ; « Ce n'est pas moi » la déconnecte sur un navigateur partagé ; un lien collé dans le même onglet est relu. - Coller ou couper un mot de quantité n'est pas une coquille (« sur tout » → « surtout », « par tout » → « partout »). - Libellés sans caractère de contrôle ; la soumission revérifie le titre. Boucle : 68 tests Rust, svelte-check, 9 vitest, e2e 13/13. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -66,6 +66,7 @@ export const en: Messages = {
|
||||
'team.join': 'Join',
|
||||
'team.signed': 'Your session is open under the name {name}: these groups are added to your identity.',
|
||||
'team.add': 'Add these groups',
|
||||
'team.notme': "It's not me: sign out",
|
||||
|
||||
'groups.title': 'My groups',
|
||||
'groups.same.key': 'Groups that use the same key',
|
||||
@@ -287,7 +288,7 @@ export const en: Messages = {
|
||||
'access.invite.cancel': 'Cancel',
|
||||
'access.invite.pending': 'Pending',
|
||||
'access.team': 'Team links',
|
||||
'access.team.intro': 'A link to share with a team, until a date. Each person who opens it gives their name and receives their own identity, never the administrator role.',
|
||||
'access.team.intro': 'A link to share with a team, until a date. Each person who opens it gives their name and receives their own identity, never the administrator role. Revoking closes the link; people who already came keep their access, to be removed one by one.',
|
||||
'access.team.label': 'For which team',
|
||||
'access.team.days': 'Valid for how many days',
|
||||
'access.team.uses': 'How many people at most',
|
||||
@@ -334,11 +335,11 @@ export const en: Messages = {
|
||||
'error.g1_not_configured': "Ğ1 sign-in isn't active yet.",
|
||||
'error.invitation_invalid': 'This invitation link is no longer valid.',
|
||||
'error.identity_kind': 'A new link can only be for an invited person, never a Ğ1 identity.',
|
||||
'error.hidden_characters': 'The text contains invisible characters (writing-direction overrides, zero-width spaces): remove them.',
|
||||
'error.hidden_characters': 'The text contains invisible characters (writing-direction override, zero-width space, soft hyphen, emoji colour selector…): remove them.',
|
||||
'error.changed': 'The text has changed in the meantime: reload it, then try again.',
|
||||
'error.wot_members_only': 'Here, only members of the Ğ1 web of trust vote.',
|
||||
'error.label': 'The name is empty or too long (60 characters at most).',
|
||||
'error.name': 'Your name has 2 to 60 characters, with no invisible character.',
|
||||
'error.name': 'Your name has 2 to 60 characters, on one line: letters, figures, spaces, hyphen, apostrophe or dot.',
|
||||
'error.name_taken': 'This name is already taken. Choose another one, adding an initial for instance.',
|
||||
'error.team_limits': 'A team link lasts 1 to 90 days, for 1 to 500 people.',
|
||||
'error.groups': 'Choose at least one group.',
|
||||
|
||||
@@ -66,6 +66,7 @@ export const es: Messages = {
|
||||
'team.join': 'Unirme',
|
||||
'team.signed': 'Tu sesión está abierta con el nombre {name}: estos grupos se añaden a tu identidad.',
|
||||
'team.add': 'Añadir estos grupos',
|
||||
'team.notme': 'No soy yo: cerrar sesión',
|
||||
|
||||
'groups.title': 'Mis grupos',
|
||||
'groups.same.key': 'Grupos que usan la misma clave',
|
||||
@@ -287,7 +288,7 @@ export const es: Messages = {
|
||||
'access.invite.cancel': 'Cancelar',
|
||||
'access.invite.pending': 'Pendiente',
|
||||
'access.team': 'Enlaces de equipo',
|
||||
'access.team.intro': 'Un enlace para compartir con un equipo, hasta una fecha. Cada persona que lo abre da su nombre y recibe su propia identidad, nunca el rol de administrador.',
|
||||
'access.team.intro': 'Un enlace para compartir con un equipo, hasta una fecha. Cada persona que lo abre da su nombre y recibe su propia identidad, nunca el rol de administrador. Revocar cierra el enlace; las personas que ya llegaron conservan su acceso, que se retira una por una.',
|
||||
'access.team.label': 'Para qué equipo',
|
||||
'access.team.days': 'Válido cuántos días',
|
||||
'access.team.uses': 'Cuántas personas como máximo',
|
||||
@@ -334,11 +335,11 @@ export const es: Messages = {
|
||||
'error.g1_not_configured': 'La conexión Ğ1 aún no está activa.',
|
||||
'error.invitation_invalid': 'Este enlace de invitación ya no es válido.',
|
||||
'error.identity_kind': 'Un nuevo enlace solo puede ser para una persona invitada, nunca para una identidad Ğ1.',
|
||||
'error.hidden_characters': 'El texto contiene caracteres invisibles (inversión del sentido de escritura, espacios de ancho cero): quítalos.',
|
||||
'error.hidden_characters': 'El texto contiene caracteres invisibles (inversión del sentido de escritura, espacio de ancho cero, guion condicional, selector de color de un emoji…): quítalos.',
|
||||
'error.changed': 'El texto ha cambiado mientras tanto: vuelve a cargarlo y empieza de nuevo.',
|
||||
'error.wot_members_only': 'Aquí solo votan los miembros de la red de confianza Ğ1.',
|
||||
'error.label': 'El nombre está vacío o es demasiado largo (60 caracteres como máximo).',
|
||||
'error.name': 'Tu nombre tiene de 2 a 60 caracteres, sin caracteres invisibles.',
|
||||
'error.name': 'Tu nombre tiene de 2 a 60 caracteres, en una línea: letras, cifras, espacios, guion, apóstrofo o punto.',
|
||||
'error.name_taken': 'Este nombre ya está ocupado. Elige otro, añadiendo una inicial por ejemplo.',
|
||||
'error.team_limits': 'Un enlace de equipo vale de 1 a 90 días, para 1 a 500 personas.',
|
||||
'error.groups': 'Elige al menos un grupo.',
|
||||
|
||||
@@ -69,6 +69,7 @@ export const fr = {
|
||||
'team.join': 'Rejoindre',
|
||||
'team.signed': 'Ta session est ouverte sous le nom {name} : ces groupes s’ajoutent à ton identité.',
|
||||
'team.add': 'Ajouter ces groupes',
|
||||
'team.notme': "Ce n'est pas moi : me déconnecter",
|
||||
|
||||
'groups.title': 'Mes groupes',
|
||||
'groups.same.key': 'Groupes qui utilisent la même clé',
|
||||
@@ -290,7 +291,7 @@ export const fr = {
|
||||
'access.invite.cancel': 'Annuler',
|
||||
'access.invite.pending': 'En attente',
|
||||
'access.team': "Liens d'équipe",
|
||||
'access.team.intro': "Un lien à partager avec une équipe, jusqu'à une date. Chaque personne qui l'ouvre donne son nom et reçoit sa propre identité, jamais le rôle d'administrateur.",
|
||||
'access.team.intro': "Un lien à partager avec une équipe, jusqu'à une date. Chaque personne qui l'ouvre donne son nom et reçoit sa propre identité, jamais le rôle d'administrateur. Révoquer ferme le lien ; les personnes déjà venues gardent leurs accès, à retirer une par une.",
|
||||
'access.team.label': 'Pour quelle équipe',
|
||||
'access.team.days': 'Valable combien de jours',
|
||||
'access.team.uses': 'Combien de personnes au plus',
|
||||
@@ -337,11 +338,11 @@ export const fr = {
|
||||
'error.g1_not_configured': "La connexion Ğ1 n'est pas encore active.",
|
||||
'error.invitation_invalid': "Ce lien d'invitation n'est plus valable.",
|
||||
'error.identity_kind': "Un nouveau lien ne peut viser qu'une personne invitée, jamais une identité Ğ1.",
|
||||
'error.hidden_characters': 'Le texte contient des caractères invisibles (inversion du sens d\'écriture, espaces de largeur nulle) : retire-les.',
|
||||
'error.hidden_characters': "Le texte contient des caractères invisibles (inversion du sens d'écriture, espace de largeur nulle, trait d'union conditionnel, sélecteur de couleur d'un emoji…) : retire-les.",
|
||||
'error.changed': 'Le texte a changé entre-temps : recharge-le, puis recommence.',
|
||||
'error.wot_members_only': 'Ici, seuls les membres de la toile de confiance Ğ1 votent.',
|
||||
'error.label': 'Le nom est vide ou trop long (60 signes au plus).',
|
||||
'error.name': 'Ton nom fait de 2 à 60 signes, sans caractère invisible.',
|
||||
'error.name': "Ton nom fait de 2 à 60 signes, sur une ligne : lettres, chiffres, espaces, trait d'union, apostrophe ou point.",
|
||||
'error.name_taken': 'Ce nom est déjà pris. Choisis-en un autre, en ajoutant une initiale par exemple.',
|
||||
'error.team_limits': "Un lien d'équipe vaut de 1 à 90 jours, pour 1 à 500 personnes.",
|
||||
'error.groups': 'Choisis au moins un groupe.',
|
||||
|
||||
@@ -20,9 +20,10 @@
|
||||
let error = $state('');
|
||||
let busy = $state(false);
|
||||
|
||||
onMount(async () => {
|
||||
async function read() {
|
||||
// The token travels in the fragment: it never reaches a server log.
|
||||
token = location.hash.slice(1).trim();
|
||||
const fragment = location.hash.slice(1).trim();
|
||||
if (fragment) token = fragment;
|
||||
history.replaceState(null, '', '/equipe');
|
||||
if (!/^[0-9a-f]{64}$/.test(token)) {
|
||||
phase = 'missing';
|
||||
@@ -34,8 +35,22 @@
|
||||
} catch {
|
||||
phase = 'invalid';
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
void read();
|
||||
// A whole link pasted into this same tab only changes the fragment.
|
||||
const again = () => void read();
|
||||
window.addEventListener('hashchange', again);
|
||||
return () => window.removeEventListener('hashchange', again);
|
||||
});
|
||||
|
||||
// A shared browser: whoever is signed in is not the person opening the link.
|
||||
async function notMe() {
|
||||
await session.logout();
|
||||
await read();
|
||||
}
|
||||
|
||||
async function join(e: SubmitEvent) {
|
||||
e.preventDefault();
|
||||
error = '';
|
||||
@@ -62,7 +77,10 @@
|
||||
<form class="card block" onsubmit={join}>
|
||||
{#if link.signedInAs}
|
||||
<p>{t('team.signed', { name: link.signedInAs })}</p>
|
||||
<button class="btn primary" type="submit" disabled={busy}>{t('team.add')}</button>
|
||||
<div class="row">
|
||||
<button class="btn primary" type="submit" disabled={busy}>{t('team.add')}</button>
|
||||
<button class="btn quiet" type="button" onclick={notMe}>{t('team.notme')}</button>
|
||||
</div>
|
||||
{:else}
|
||||
<label class="field">
|
||||
<span>{t('team.name')}</span>
|
||||
@@ -102,6 +120,11 @@
|
||||
.block > .btn {
|
||||
justify-self: start;
|
||||
}
|
||||
.row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
.small {
|
||||
font-size: 0.84rem;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user