Redesign settings/repo-settings, add missing delete confirmations
Brings both settings pages up to the visual language established elsewhere this session (icons, list-row components, card-based layout instead of bare tables) — approved from an Artifact mockup first. Also adds the two destructive actions that had no confirmation prompt: revoking a collaborator and removing an SSH key (warns specifically about losing SSH access if it's your last one).
6 files changed
+176 −106
M
CHANGELOG.md
+5 −0
M
cmd/gitfed-web/handlers_repo.go
+46 −24
M
cmd/gitfed-web/handlers_settings.go
+31 −16
M
cmd/gitfed-web/render.go
+20 −4
M
internal/i18n/strings_en.go
+37 −31
M
internal/i18n/strings_fr.go
+37 −31
CHANGELOG.md
@@ -1,5 +1,10 @@
# Changelog
+## 0.9.1
+
+- Settings and repo-settings visually brought up to date with the rest of the app: icons on the settings tabs, a real profile card, collaborators shown as icon+role rows instead of a bare table (with a shield for the owner, a globe for federated collaborators), and a visually distinct danger zone.
+- Added confirmation prompts to two destructive actions that didn't have one: revoking a collaborator, and removing an SSH key (the latter warns if it might be your only one).
+
## 0.9.0
- **Pinned remote repos**: bookmark a repo on another instance from your dashboard — works instantly for public repos (no login needed there), and just gives a link for a private one you already have git access to.
cmd/gitfed-web/handlers_repo.go
@@ -71,6 +71,14 @@ type treeEntryView struct {
FullPath string
}
+// collaboratorView adds display-only context to a store.Collaborator: only
+// the settings page needs to distinguish local from federated principals,
+// so it's computed there rather than carried on the ACL model itself.
+type collaboratorView struct {
+ store.Collaborator
+ Remote bool
+}
+
// repoTpl renders the repo's file tree at the current path, GitLab-style:
// files/dirs at the top, README (and, root only, LICENSE) rendered directly
// below — no separate "browse files" page.
@@ -339,8 +347,8 @@ var plainTpl = template.Must(template.New("plain").Parse(`<pre>{{.}}</pre>`))
var repoSettingsTpl = newTpl("repo-settings", `
{{.Flash}}
-<p><a href="/r/{{.Repo}}">← {{.Repo}}</a></p>
-<h1>{{.Repo}} {{t .Lang "repo.settings_title"}}</h1>
+<div class="gf-crumbs"><a href="/r/{{.Repo}}">{{.Repo}}</a></div>
+<div class="gf-page-head"><h1>{{t .Lang "repo.settings_title"}}</h1></div>
<form class="card" method="post" action="/repo-settings/{{.Repo}}">
<strong>{{t .Lang "repo.visibility_topics"}}</strong>
@@ -350,26 +358,31 @@ var repoSettingsTpl = newTpl("repo-settings", `
<button type="submit">{{t .Lang "repo.save"}}</button>
</form>
-<table>
-<tr><th>{{t .Lang "repo.col_principal"}}</th><th>{{t .Lang "repo.col_role"}}</th><th></th></tr>
-<tr>
- <td>{{.Owner}}</td>
- <td>{{t .Lang "role.admin"}}</td>
- <td class="muted">{{t .Lang "role.owner"}}</td>
-</tr>
+<h3 style="font-size:0.95rem; margin-bottom:0.6rem;">{{t .Lang "repo.collaborators_title"}}</h3>
+<div class="gf-card">
+ <div class="gf-list-row">
+ <div class="gf-list-icon">{{icon "shield"}}</div>
+ <div class="gf-list-main">
+ <div class="primary">{{.Owner}}</div>
+ <div class="meta">{{t .Lang "role.owner"}}</div>
+ </div>
+ <span class="badge plain">{{t .Lang "role.admin"}}</span>
+ </div>
{{range .Collaborators}}
-<tr>
- <td>{{.Principal}}</td>
- <td>{{roleLabel $.Lang (print .Role)}}</td>
- <td>
- <form class="inline" method="post" action="/repo-revoke/{{$.Repo}}">
+ <div class="gf-list-row">
+ <div class="gf-list-icon">{{if .Remote}}{{icon "network"}}{{else}}{{icon "user"}}{{end}}</div>
+ <div class="gf-list-main">
+ <div class="primary">{{.Principal}}</div>
+ <div class="meta">{{if .Remote}}{{t $.Lang "repo.federated_collaborator"}}{{else}}{{t $.Lang "repo.local_collaborator"}}{{end}}</div>
+ </div>
+ <span class="badge plain">{{roleLabel $.Lang (print .Role)}}</span>
+ <form class="inline" method="post" action="/repo-revoke/{{$.Repo}}" data-confirm="{{t $.Lang "repo.confirm_revoke"}} {{.Principal}}?">
<input type="hidden" name="principal" value="{{.Principal}}">
- <button class="danger" type="submit">{{t $.Lang "repo.revoke"}}</button>
+ <button class="linklike" type="submit" title="{{t $.Lang "repo.revoke"}}" aria-label="{{t $.Lang "repo.revoke"}}">{{icon "close"}}</button>
</form>
- </td>
-</tr>
+ </div>
{{end}}
-</table>
+</div>
<form class="card" method="post" action="/repo-grant/{{.Repo}}">
<strong>{{t .Lang "repo.grant_collaborator"}}</strong>
@@ -383,12 +396,15 @@ var repoSettingsTpl = newTpl("repo-settings", `
</select>
<button type="submit">{{t .Lang "repo.grant"}}</button>
</form>
-<p class="muted">{{t .Lang "repo.grant_note"}}</p>
+<p class="muted" style="margin-top:-0.9rem;">{{t .Lang "repo.grant_note"}}</p>
-<form class="card" method="post" action="/repo-delete/{{.Repo}}" data-confirm="{{t .Lang "repo.confirm_delete"}} {{.Repo}}?">
+<div class="gf-danger-zone">
<strong>{{t .Lang "repo.danger_zone"}}</strong>
- <button class="danger" type="submit">{{t .Lang "repo.delete_record"}}</button>
-</form>
+ <p class="muted">{{t .Lang "repo.danger_zone_note"}}</p>
+ <form method="post" action="/repo-delete/{{.Repo}}" data-confirm="{{t .Lang "repo.confirm_delete"}} {{.Repo}}?">
+ <button class="danger" type="submit">{{t .Lang "repo.delete_record"}}</button>
+ </form>
+</div>
`)
func (s *server) handleRepoSettingsForm(w http.ResponseWriter, r *http.Request) {
@@ -409,6 +425,12 @@ func (s *server) handleRepoSettingsForm(w http.ResponseWriter, r *http.Request)
return
}
+ collaborators := make([]collaboratorView, len(acl.Collaborators))
+ for i, c := range acl.Collaborators {
+ _, domain, _ := strings.Cut(c.Principal, "@")
+ collaborators[i] = collaboratorView{Collaborator: c, Remote: domain != s.domain}
+ }
+
var buf bytes.Buffer
_ = repoSettingsTpl.Execute(&buf, struct {
Repo string
@@ -416,9 +438,9 @@ func (s *server) handleRepoSettingsForm(w http.ResponseWriter, r *http.Request)
Lang string
Public bool
TopicsCSV string
- Collaborators []store.Collaborator
+ Collaborators []collaboratorView
Flash template.HTML
- }{name, repo.Owner, string(lang), repo.Public, strings.Join(repo.Topics, ", "), acl.Collaborators, flash(r)})
+ }{name, repo.Owner, string(lang), repo.Public, strings.Join(repo.Topics, ", "), collaborators, flash(r)})
s.render(w, r, name+" "+i18n.T(lang, "repo.settings_title"), "home", template.HTML(buf.String()))
}
cmd/gitfed-web/handlers_settings.go
@@ -4,6 +4,7 @@ import (
"bytes"
"html/template"
"net/http"
+ "strings"
"gitfed/internal/i18n"
)
@@ -13,28 +14,38 @@ var settingsTpl = newTpl("settings", `
<div class="gf-page-head"><h1>{{t .Lang "settings.title"}}</h1></div>
<div class="gf-tabs">
- <button type="button" data-tab="tab-profile" class="{{if eq .ActiveTab "profile"}}active{{end}}">{{t .Lang "settings.tab_profile"}}</button>
- <button type="button" data-tab="tab-keys" class="{{if eq .ActiveTab "keys"}}active{{end}}">{{t .Lang "settings.tab_keys"}}</button>
- <button type="button" data-tab="tab-password" class="{{if eq .ActiveTab "password"}}active{{end}}">{{t .Lang "settings.tab_password"}}</button>
+ <button type="button" data-tab="tab-profile" class="{{if eq .ActiveTab "profile"}}active{{end}}">{{icon "user"}} {{t .Lang "settings.tab_profile"}}</button>
+ <button type="button" data-tab="tab-keys" class="{{if eq .ActiveTab "keys"}}active{{end}}">{{icon "key"}} {{t .Lang "settings.tab_keys"}}</button>
+ <button type="button" data-tab="tab-password" class="{{if eq .ActiveTab "password"}}active{{end}}">{{icon "lock"}} {{t .Lang "settings.tab_password"}}</button>
</div>
<div id="tab-profile" class="gf-tabpane {{if eq .ActiveTab "profile"}}active{{end}}">
- <p class="muted">{{t .Lang "settings.logged_in_as"}} <strong>{{.Username}}</strong>{{if .IsAdmin}} — {{t .Lang "role.admin"}}{{end}}.</p>
+ <div class="gf-card gf-profile-card">
+ <div class="avatar">{{.Initials}}</div>
+ <div>
+ <div class="who">{{.Username}}{{if .IsAdmin}} — <span class="admin-tag">{{t .Lang "role.admin"}}</span>{{end}}</div>
+ <div class="principal">{{.Principal}}</div>
+ </div>
+ </div>
<p class="muted">{{t .Lang "settings.profile_note"}}</p>
</div>
<div id="tab-keys" class="gf-tabpane {{if eq .ActiveTab "keys"}}active{{end}}">
<div class="gf-card">
{{range .Keys}}
- <div class="gf-key-row">
- <code>{{.Short}}</code>
- <form class="inline" method="post" action="/settings/keys/remove">
+ <div class="gf-list-row">
+ <div class="gf-list-icon">{{icon "key"}}</div>
+ <div class="gf-list-main">
+ <div class="primary">{{.Short}}</div>
+ <div class="meta">{{.Algo}}</div>
+ </div>
+ <form class="inline" method="post" action="/settings/keys/remove" data-confirm="{{t $.Lang "settings.confirm_remove_key"}}">
<input type="hidden" name="key" value="{{.Full}}">
<button class="danger" type="submit">{{t $.Lang "settings.remove"}}</button>
</form>
</div>
{{else}}
- <div class="gf-key-row"><span class="muted">{{t .Lang "settings.no_keys"}}</span></div>
+ <div class="gf-list-row"><span class="muted">{{t .Lang "settings.no_keys"}}</span></div>
{{end}}
</div>
@@ -58,7 +69,7 @@ var settingsTpl = newTpl("settings", `
</div>
`)
-type keyView struct{ Short, Full string }
+type keyView struct{ Short, Full, Algo string }
func settingsTab(r *http.Request) string {
switch r.URL.Query().Get("tab") {
@@ -82,20 +93,24 @@ func (s *server) handleSettings(w http.ResponseWriter, r *http.Request) {
keys := make([]keyView, len(user.PubKeys))
for i, k := range user.PubKeys {
+ algo, _, ok := strings.Cut(k, " ")
+ if !ok {
+ algo = "?"
+ }
short := k
if len(short) > 60 {
- short = short[:60] + "..."
+ short = short[:60] + "…"
}
- keys[i] = keyView{Short: short, Full: k}
+ keys[i] = keyView{Short: short, Full: k, Algo: algo}
}
var buf bytes.Buffer
_ = settingsTpl.Execute(&buf, struct {
- Username, ActiveTab, Lang string
- IsAdmin bool
- Keys []keyView
- Flash template.HTML
- }{sess.Username, settingsTab(r), string(lang), sess.IsAdmin, keys, flash(r)})
+ Username, Principal, ActiveTab, Lang, Initials string
+ IsAdmin bool
+ Keys []keyView
+ Flash template.HTML
+ }{sess.Username, sess.Principal, settingsTab(r), string(lang), initials(sess.Username), sess.IsAdmin, keys, flash(r)})
s.render(w, r, i18n.T(lang, "settings.title"), "settings", template.HTML(buf.String()))
}
cmd/gitfed-web/render.go
@@ -141,6 +141,7 @@ const iconSprite = `<svg width="0" height="0" style="position:absolute" aria-hid
<symbol id="ic-close" viewBox="0 0 24 24"><path d="M5.5 5.5l13 13M18.5 5.5l-13 13" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/></symbol>
<symbol id="ic-bell" viewBox="0 0 24 24"><path d="M6 10.5c0-3.3 2.7-6 6-6s6 2.7 6 6v3.3l1.6 2.7H4.4L6 13.8V10.5Z" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round"/><path d="M9.8 18.5a2.3 2.3 0 0 0 4.4 0" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></symbol>
<symbol id="ic-history" viewBox="0 0 24 24"><circle cx="12" cy="12.5" r="8" fill="none" stroke="currentColor" stroke-width="1.5"/><path d="M12 8v4.7l3.3 2" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/><path d="M8 3.3 5 6M16 3.3 19 6" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></symbol>
+<symbol id="ic-user" viewBox="0 0 24 24"><circle cx="12" cy="8.3" r="3.3" fill="none" stroke="currentColor" stroke-width="1.5"/><path d="M4.8 19c1-3.2 3.9-5 7.2-5s6.2 1.8 7.2 5" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></symbol>
</defs>
</svg>`
@@ -388,10 +389,25 @@ const shellHeadSrc = `<!doctype html>
.gf-tabpane { display: none; }
.gf-tabpane.active { display: block; }
- .gf-key-row { display: flex; align-items: center; gap: 0.9rem; padding: 0.8rem 1.1rem; border-bottom: 1px solid var(--border); }
- .gf-key-row:last-child { border-bottom: none; }
- .gf-key-row code { font-size: 0.82rem; color: var(--text-dim); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex: 1; background: none; padding: 0; }
- .gf-key-row button { margin: 0; flex-shrink: 0; }
+ .gf-profile-card { display: flex; align-items: center; gap: 0.9rem; padding: 1.1rem; }
+ .gf-profile-card .avatar { width: 46px; height: 46px; border-radius: 50%; background: var(--accent-dim); color: var(--text); display: flex; align-items: center; justify-content: center; font-size: 0.95rem; font-weight: 700; flex-shrink: 0; }
+ .gf-profile-card .who { font-size: 1.02rem; font-weight: 700; }
+ .gf-profile-card .who .admin-tag { color: var(--accent); font-weight: 600; }
+ .gf-profile-card .principal { font-family: var(--mono); font-size: 0.8rem; color: var(--text-faint); margin-top: 0.15rem; }
+
+ .gf-list-row { display: flex; align-items: center; gap: 0.9rem; padding: 0.85rem 1.1rem; border-bottom: 1px solid var(--border); }
+ .gf-list-row:last-child { border-bottom: none; }
+ .gf-list-row:hover { background: var(--surface-2); }
+ .gf-list-icon { width: 30px; height: 30px; border-radius: 7px; background: var(--surface-2); display: flex; align-items: center; justify-content: center; flex-shrink: 0; color: var(--text-dim); }
+ .gf-list-main { flex: 1; min-width: 0; }
+ .gf-list-main .primary { font-family: var(--mono); font-size: 0.86rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
+ .gf-list-main .meta { font-size: 0.76rem; color: var(--text-faint); margin-top: 0.15rem; }
+ .gf-list-row button { margin: 0; flex-shrink: 0; }
+
+ .gf-danger-zone { border: 1px solid rgba(242,139,130,0.35); background: rgba(242,139,130,0.05); border-radius: 10px; padding: 1.1rem; margin: 1rem 0; }
+ .gf-danger-zone strong { color: var(--danger-fg); font-size: 0.94rem; }
+ .gf-danger-zone p { margin: 0.4rem 0 0; }
+ .gf-danger-zone button { margin-top: 0.8rem; }
.gf-admin-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 1rem; margin-bottom: 1.5rem; }
.gf-admin-card { background: var(--surface); border: 1px solid var(--border); border-radius: 10px; padding: 1.1rem; display: flex; flex-direction: column; gap: 0.5rem; }
internal/i18n/strings_en.go
@@ -160,6 +160,7 @@ var en = map[string]string{
"settings.logged_in_as": "Logged in as",
"settings.profile_note": "This password only signs you into the web UI. Git push/pull always goes over SSH with a key, independently of it.",
"settings.remove": "Remove",
+ "settings.confirm_remove_key": "Remove this key? If it's your only one, you'll lose SSH/git access until you add another.",
"settings.no_keys": "No keys yet.",
"settings.add_key": "Add SSH key",
"settings.pubkey_label": "Public key (authorized_keys format)",
@@ -240,35 +241,40 @@ var en = map[string]string{
"admin.audit_note": "Showing the most recent 200 events.",
// ---------- repo ----------
- "repo.owner": "owner",
- "repo.copy_clone_url": "Copy clone URL",
- "repo.read_only": "read-only",
- "repo.https_readonly_hint": "Anonymous clone/fetch only — pushing over HTTPS is never possible, even for a public repo. Push always goes over SSH.",
- "repo.empty": "This repository is empty.",
- "repo.directory": "directory",
- "repo.file": "file",
- "repo.nothing_here": "Nothing here.",
- "repo.tags": "Tags",
- "repo.commits_title": "Commits",
- "repo.commits_empty": "No commits yet.",
- "repo.license": "License",
- "repo.binary_file": "Binary file — not shown.",
- "repo.settings_title": "settings",
- "repo.visibility_topics": "Visibility & topics",
- "repo.public_desc": "Public (readable by any authenticated principal)",
- "repo.topics_label": "Topics (comma-separated)",
- "repo.save": "Save",
- "repo.col_principal": "Principal",
- "repo.col_role": "Role",
- "repo.revoke": "Revoke",
- "repo.grant_collaborator": "Grant collaborator",
- "repo.grant": "Grant",
- "repo.grant_note": "Granting a collaborator on a domain not yet known to this instance triggers federation discovery of that domain's CA — an instance admin needs to approve it under Admin → Trust store (whitelist policy).",
- "repo.confirm_delete": "Delete",
- "repo.danger_zone": "Danger zone",
- "repo.delete_record": "Delete repo record",
- "repo.msg_saved": "saved settings",
- "repo.msg_granted": "granted %s %s",
- "repo.msg_revoked": "revoked %s",
- "repo.msg_deleted": "deleted %s",
+ "repo.owner": "owner",
+ "repo.copy_clone_url": "Copy clone URL",
+ "repo.read_only": "read-only",
+ "repo.https_readonly_hint": "Anonymous clone/fetch only — pushing over HTTPS is never possible, even for a public repo. Push always goes over SSH.",
+ "repo.empty": "This repository is empty.",
+ "repo.directory": "directory",
+ "repo.file": "file",
+ "repo.nothing_here": "Nothing here.",
+ "repo.tags": "Tags",
+ "repo.commits_title": "Commits",
+ "repo.commits_empty": "No commits yet.",
+ "repo.license": "License",
+ "repo.binary_file": "Binary file — not shown.",
+ "repo.settings_title": "settings",
+ "repo.visibility_topics": "Visibility & topics",
+ "repo.public_desc": "Public (readable by any authenticated principal)",
+ "repo.topics_label": "Topics (comma-separated)",
+ "repo.save": "Save",
+ "repo.col_principal": "Principal",
+ "repo.col_role": "Role",
+ "repo.revoke": "Revoke",
+ "repo.grant_collaborator": "Grant collaborator",
+ "repo.grant": "Grant",
+ "repo.grant_note": "Granting a collaborator on a domain not yet known to this instance triggers federation discovery of that domain's CA — an instance admin needs to approve it under Admin → Trust store (whitelist policy).",
+ "repo.confirm_delete": "Delete",
+ "repo.confirm_revoke": "Revoke access for",
+ "repo.danger_zone": "Danger zone",
+ "repo.danger_zone_note": "Deleting removes the repo's record from gitfed — it does not delete the bare repo on disk.",
+ "repo.collaborators_title": "Collaborators",
+ "repo.federated_collaborator": "federated collaborator",
+ "repo.local_collaborator": "local collaborator",
+ "repo.delete_record": "Delete repo record",
+ "repo.msg_saved": "saved settings",
+ "repo.msg_granted": "granted %s %s",
+ "repo.msg_revoked": "revoked %s",
+ "repo.msg_deleted": "deleted %s",
}
internal/i18n/strings_fr.go
@@ -160,6 +160,7 @@ var fr = map[string]string{
"settings.logged_in_as": "Connecté en tant que",
"settings.profile_note": "Ce mot de passe ne sert qu'à se connecter à l'interface web. Le push/pull git passe toujours par SSH avec une clé, indépendamment de lui.",
"settings.remove": "Retirer",
+ "settings.confirm_remove_key": "Retirer cette clé ? Si c'est la seule, vous perdrez l'accès SSH/git jusqu'à en ajouter une autre.",
"settings.no_keys": "Aucune clé pour le moment.",
"settings.add_key": "Ajouter une clé SSH",
"settings.pubkey_label": "Clé publique (format authorized_keys)",
@@ -240,35 +241,40 @@ var fr = map[string]string{
"admin.audit_note": "Affichage des 200 événements les plus récents.",
// ---------- repo ----------
- "repo.owner": "propriétaire",
- "repo.copy_clone_url": "Copier l'URL de clonage",
- "repo.read_only": "lecture seule",
- "repo.https_readonly_hint": "Clone/fetch anonyme uniquement — impossible de pousser en HTTPS, même sur un dépôt public. Le push passe toujours par SSH.",
- "repo.empty": "Ce dépôt est vide.",
- "repo.directory": "dossier",
- "repo.file": "fichier",
- "repo.nothing_here": "Rien ici.",
- "repo.tags": "Tags",
- "repo.commits_title": "Commits",
- "repo.commits_empty": "Aucun commit pour le moment.",
- "repo.license": "Licence",
- "repo.binary_file": "Fichier binaire — non affiché.",
- "repo.settings_title": "paramètres",
- "repo.visibility_topics": "Visibilité et sujets",
- "repo.public_desc": "Public (lisible par tout principal authentifié)",
- "repo.topics_label": "Sujets (séparés par des virgules)",
- "repo.save": "Enregistrer",
- "repo.col_principal": "Principal",
- "repo.col_role": "Rôle",
- "repo.revoke": "Révoquer",
- "repo.grant_collaborator": "Ajouter un collaborateur",
- "repo.grant": "Accorder",
- "repo.grant_note": "Accorder un accès à un domaine encore inconnu de cette instance déclenche la découverte fédérée de la CA de ce domaine — un administrateur de l'instance doit l'approuver dans Administration → Confiance fédérée (politique de liste blanche).",
- "repo.confirm_delete": "Supprimer",
- "repo.danger_zone": "Zone de danger",
- "repo.delete_record": "Supprimer l'entrée du dépôt",
- "repo.msg_saved": "paramètres enregistrés",
- "repo.msg_granted": "%s ajouté avec le rôle %s",
- "repo.msg_revoked": "%s révoqué",
- "repo.msg_deleted": "%s supprimé",
+ "repo.owner": "propriétaire",
+ "repo.copy_clone_url": "Copier l'URL de clonage",
+ "repo.read_only": "lecture seule",
+ "repo.https_readonly_hint": "Clone/fetch anonyme uniquement — impossible de pousser en HTTPS, même sur un dépôt public. Le push passe toujours par SSH.",
+ "repo.empty": "Ce dépôt est vide.",
+ "repo.directory": "dossier",
+ "repo.file": "fichier",
+ "repo.nothing_here": "Rien ici.",
+ "repo.tags": "Tags",
+ "repo.commits_title": "Commits",
+ "repo.commits_empty": "Aucun commit pour le moment.",
+ "repo.license": "Licence",
+ "repo.binary_file": "Fichier binaire — non affiché.",
+ "repo.settings_title": "paramètres",
+ "repo.visibility_topics": "Visibilité et sujets",
+ "repo.public_desc": "Public (lisible par tout principal authentifié)",
+ "repo.topics_label": "Sujets (séparés par des virgules)",
+ "repo.save": "Enregistrer",
+ "repo.col_principal": "Principal",
+ "repo.col_role": "Rôle",
+ "repo.revoke": "Révoquer",
+ "repo.grant_collaborator": "Ajouter un collaborateur",
+ "repo.grant": "Accorder",
+ "repo.grant_note": "Accorder un accès à un domaine encore inconnu de cette instance déclenche la découverte fédérée de la CA de ce domaine — un administrateur de l'instance doit l'approuver dans Administration → Confiance fédérée (politique de liste blanche).",
+ "repo.confirm_delete": "Supprimer",
+ "repo.confirm_revoke": "Révoquer l'accès de",
+ "repo.danger_zone": "Zone de danger",
+ "repo.danger_zone_note": "La suppression retire l'entrée du dépôt de gitfed — elle ne supprime pas le dépôt bare sur le disque.",
+ "repo.collaborators_title": "Collaborateurs",
+ "repo.federated_collaborator": "collaborateur fédéré",
+ "repo.local_collaborator": "collaborateur local",
+ "repo.delete_record": "Supprimer l'entrée du dépôt",
+ "repo.msg_saved": "paramètres enregistrés",
+ "repo.msg_granted": "%s ajouté avec le rôle %s",
+ "repo.msg_revoked": "%s révoqué",
+ "repo.msg_deleted": "%s supprimé",
}