oficina / estilo Public

Door copy nits: name the spec, tokens not formats, email from the set

Fable 5.1's low findings on estilo #19 (feat/door-copy):

- The "spec §9.2/§10" citations now name the document: the shared-chrome
  spec, docs/superpowers/specs/2026-09-23-shared-chrome.md on estilo's
  spec/shared-chrome branch (finding 2).
- The email is part of the copy set: DoorCopy.SignInEmail, MailSubject
  and MailBody read the set's EmailSubject/EmailBody, so a locale's
  email is reachable. The package SignInEmail, EmailSubject and
  EmailBody are English's, unchanged for every caller (finding 3).
- Slots are {app} and {link} tokens, filled in one pass, not Printf
  verbs: a translation without the slot, or with a literal "%", renders
  as written instead of "%!(EXTRA …)" (finding 4). No app sets Copy, so
  the format change breaks nobody; English's words are unchanged.
- var minutes is linkMinutes (finding 6).
- DoorCopy states the plan: translations are estilo's follow-up, never
  an app's (finding 5).

TestDoorCopyFillsTokensNotFormats and TestSignInEmailReadsTheCopy failed
first (against stubs delegating to the English package helpers) and
pass now; every existing pin is unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Paul Campbell pushed by paul@keymail.dev 1a41328e52df20740934ad9237e92de2794a1773 parent 0b4d92c
2 files changed, +116 −33
  • chrome/door.go +61 −27
  • chrome/door_test.go +55 −6

CI passed — run details

CI log
=== modcache ===
all modules verified
=== vet ===
CGO_ENABLED=0 go vet ./...
=== fmt ===
=== build ===
CGO_ENABLED=0 go build -buildvcs=false ./...
=== go-test ===
CGO_ENABLED=0 go test -count=1 ./...
ok  	amadan.net/oficina/estilo.git/base	0.004s
ok  	amadan.net/oficina/estilo.git/brand	0.002s
ok  	amadan.net/oficina/estilo.git/busy	0.002s
ok  	amadan.net/oficina/estilo.git/callui	0.004s
ok  	amadan.net/oficina/estilo.git/chrome	0.218s
?   	amadan.net/oficina/estilo.git/cmd/oficina-notices	[no test files]
ok  	amadan.net/oficina/estilo.git/credits	0.005s
ok  	amadan.net/oficina/estilo.git/credits/catalog	0.005s
ok  	amadan.net/oficina/estilo.git/homeconnect	0.004s
ok  	amadan.net/oficina/estilo.git/internal/lint	0.003s
?   	amadan.net/oficina/estilo.git/internal/tokencheck	[no test files]
ok  	amadan.net/oficina/estilo.git/menu	0.008s
ok  	amadan.net/oficina/estilo.git/savestatus	0.074s
ok  	amadan.net/oficina/estilo.git/sky	0.020s
ok  	amadan.net/oficina/estilo.git/theme	0.005s
diff --git a/chrome/door.go b/chrome/door.go
index d136e2d..946b7b6 100644
--- a/chrome/door.go
+++ b/chrome/door.go
@@ -20,6 +20,22 @@ const LinkLifetime = 15 * time.Minute
// suite's one copy set; a locale replaces the whole set through
// SignIn.Copy and SignInNotice.Copy, never a word at a time, so no app
// can drift from the others by overriding one line.
+//
+// The words are the copy deck in §9.2 of the shared-chrome spec
+// (docs/superpowers/specs/2026-09-23-shared-chrome.md, on estilo's
+// spec/shared-chrome branch), with Paul's later rulings noted where they
+// apply. "§9.2" and "§10" below are that document's sections.
+//
+// Translations are estilo's follow-up, never an app's. English is the
+// only set today. When the suite needs another locale, estilo adds its
+// whole DoorCopy here, beside English, with a way to pick one by locale,
+// and every app gets it on its next re-pin. An app passes Copy only a set
+// estilo publishes; it never writes or edits one, or the one copy set
+// fragments again the first time an app translates its own door.
+//
+// Slots are tokens, not Printf verbs: {app} is the app's name, and {link}
+// (the email body only) is the sign-in link. A translation may drop or
+// move a slot, or carry a literal "%", and still render as written.
type DoorCopy struct {
// The form: heading (also the tab title's page), address label, the
// one help line under the field, and the submit.
@@ -28,33 +44,32 @@ type DoorCopy struct {
// body, and the link back to the form.
SentHeading, SentBody, Again string
// The signed-out page: the tab title's page, the heading, the body
- // (a format with one %s, the app's name) and the Sign in button.
+ // ({app} is the app's name) and the Sign in button.
SignedOutPage, SignedOutHeading, SignedOutBody, SignIn string
- // The sign-in email: subject and body formats. The subject takes the
- // app's name; the body the app's name, then the link.
+ // The sign-in email: subject ({app}) and body ({app} and {link}).
EmailSubject, EmailBody string
}
-// minutes is LinkLifetime in words, so the number lives in one place.
-var minutes = fmt.Sprintf("%d minutes", int(LinkLifetime/time.Minute))
+// linkMinutes is LinkLifetime in words, so the number lives in one place.
+var linkMinutes = fmt.Sprintf("%d minutes", int(LinkLifetime/time.Minute))
-// English is the door's copy set (spec §9.2). The submit is "Sign in":
-// Paul, 2026-10-01, ruled out "Email me a link". The hub is never named
-// ("Home"); the suite is Oficina (Paul, 2026-09-23).
+// English is the door's copy set (shared-chrome spec §9.2). The submit
+// is "Sign in": Paul, 2026-10-01, ruled out "Email me a link". The hub is
+// never named ("Home"); the suite is Oficina (Paul, 2026-09-23).
var English = DoorCopy{
Heading: "Sign in",
AddressLabel: "Email address",
- Help: "We’ll email you a link that signs you in. It works once and expires in " + minutes + ".",
+ Help: "We’ll email you a link that signs you in. It works once and expires in " + linkMinutes + ".",
Submit: "Sign in",
SentHeading: "Check your email",
- SentBody: "If that address can sign in here, we’ve sent it a link. It works once and expires in " + minutes + ".",
+ SentBody: "If that address can sign in here, we’ve sent it a link. It works once and expires in " + linkMinutes + ".",
Again: "Use a different address",
SignedOutPage: "Signed out",
SignedOutHeading: "You’re signed out",
- SignedOutBody: "You’ve signed out of %s on this device.",
+ SignedOutBody: "You’ve signed out of {app} on this device.",
SignIn: "Sign in",
- EmailSubject: "Sign in to %s",
- EmailBody: "Here’s your link to sign in to %s:\n\n%s\n\nIt works once and expires in " + minutes + ". If you didn’t ask for it, you can ignore this email.",
+ EmailSubject: "Sign in to {app}",
+ EmailBody: "Here’s your link to sign in to {app}:\n\n{link}\n\nIt works once and expires in " + linkMinutes + ". If you didn’t ask for it, you can ignore this email.",
}
// signedOutBody names the app; with none it names the suite, as the hub's
@@ -63,7 +78,14 @@ func (c DoorCopy) signedOutBody(app string) string {
if app == "" {
app = "Oficina"
}
- return fmt.Sprintf(c.SignedOutBody, app)
+ return fill(c.SignedOutBody, app, "")
+}
+
+// fill puts the app's name and the link into a copy string's {app} and
+// {link} slots, in one pass: a name or link that itself contains a token
+// is not filled again.
+func fill(s, app, link string) string {
+ return strings.NewReplacer("{app}", app, "{link}", link).Replace(s)
}
func copyOf(c *DoorCopy) DoorCopy {
@@ -73,8 +95,8 @@ func copyOf(c *DoorCopy) DoorCopy {
return *c
}
-// Title is a page's tab title: "<Page> · <App>" (spec §10), or whichever
-// half is set when one is empty.
+// Title is a page's tab title: "<Page> · <App>" (shared-chrome spec
+// §10), or whichever half is set when one is empty.
func Title(page, app string) string {
page, app = strings.TrimSpace(page), strings.TrimSpace(app)
switch {
@@ -132,18 +154,30 @@ func (n SignInNotice) Page() string {
// Title is the notice's whole tab title, "<Page> · <App>".
func (n SignInNotice) Title() string { return Title(n.Page(), n.App) }
-// SignInEmail is the sign-in email for app: subject and body around link,
-// in English.
-func SignInEmail(app, link string) (subject, body string) {
- return EmailSubject(app), EmailBody(app)(link)
+// SignInEmail is this copy set's sign-in email for app: subject and body
+// around link.
+func (c DoorCopy) SignInEmail(app, link string) (subject, body string) {
+ return c.MailSubject(app), c.MailBody(app)(link)
}
-// EmailSubject is the sign-in email's subject, "Sign in to <App>": what
-// rastrillo's auth.Config.Subject takes.
-func EmailSubject(app string) string { return fmt.Sprintf(English.EmailSubject, app) }
+// MailSubject is the sign-in email's subject, "Sign in to <App>" in
+// English. (Not EmailSubject: that is the field it reads.)
+func (c DoorCopy) MailSubject(app string) string { return fill(c.EmailSubject, app, "") }
-// EmailBody renders the sign-in email's body around the link: the shape
-// keymaildev/signin's Flow.Body takes.
-func EmailBody(app string) func(link string) string {
- return func(link string) string { return fmt.Sprintf(English.EmailBody, app, link) }
+// MailBody renders the sign-in email's body around the link.
+func (c DoorCopy) MailBody(app string) func(link string) string {
+ return func(link string) string { return fill(c.EmailBody, app, link) }
}
+
+// SignInEmail is the sign-in email for app in English: subject and body
+// around link.
+func SignInEmail(app, link string) (subject, body string) { return English.SignInEmail(app, link) }
+
+// EmailSubject is the sign-in email's subject in English, "Sign in to
+// <App>": what rastrillo's auth.Config.Subject takes.
+func EmailSubject(app string) string { return English.MailSubject(app) }
+
+// EmailBody renders the English sign-in email's body around the link: the
+// shape keymaildev/signin's Flow.Body and rastrillo's auth.Config.Body
+// take.
+func EmailBody(app string) func(link string) string { return English.MailBody(app) }
diff --git a/chrome/door_test.go b/chrome/door_test.go
index 5a442ab..aa9a52d 100644
--- a/chrome/door_test.go
+++ b/chrome/door_test.go
@@ -7,9 +7,10 @@ import (
"time"
)
-// The door's copy is the suite's one copy set (spec §9.2, with Paul's
-// 2026-10-01 ruling that the submit reads "Sign in"). Pinned word for
-// word: a change here is a copy decision, made on purpose, in one place.
+// The door's copy is the suite's one copy set (shared-chrome spec §9.2,
+// with Paul's 2026-10-01 ruling that the submit reads "Sign in"). Pinned
+// word for word: a change here is a copy decision, made on purpose, in one
+// place.
func TestDoorCopyIsPinned(t *testing.T) {
if LinkLifetime != 15*time.Minute {
t.Fatalf("LinkLifetime = %v; the copy below says 15 minutes", LinkLifetime)
@@ -40,7 +41,7 @@ func TestDoorCopyIsPinned(t *testing.T) {
}
}
-// Every door page's tab title is "<Page> · <App>" (spec §10).
+// Every door page's tab title is "<Page> · <App>" (shared-chrome spec §10).
func TestDoorTitles(t *testing.T) {
for _, c := range []struct{ got, want string }{
{Title("Sign in", "Docs"), "Sign in · Docs"},
@@ -103,7 +104,7 @@ func TestTheDoorDrawsItsOwnCopy(t *testing.T) {
}
// The sign-in email names the app and says the true lifetime, the same
-// words as the door (spec §9.2).
+// words as the door (shared-chrome spec §9.2).
func TestSignInEmail(t *testing.T) {
subject, body := SignInEmail("Docs", "https://docs.example/auth/verify?token=x")
if subject != "Sign in to Docs" {
@@ -123,7 +124,7 @@ func TestDoorCopyIsReplaceablePerLocale(t *testing.T) {
for locale, label := range locales {
t.Run(locale, func(t *testing.T) {
esc := template.HTMLEscapeString
- c := &DoorCopy{Heading: label + "-h", AddressLabel: label + "-addr", Help: label + "-help", Submit: label + "-submit", SentHeading: label + "-sent", SentBody: label + "-sentbody", Again: label + "-again", SignedOutPage: label + "-outpage", SignedOutHeading: label + "-out", SignedOutBody: label + " %s-outbody", SignIn: label + "-in"}
+ c := &DoorCopy{Heading: label + "-h", AddressLabel: label + "-addr", Help: label + "-help", Submit: label + "-submit", SentHeading: label + "-sent", SentBody: label + "-sentbody", Again: label + "-again", SignedOutPage: label + "-outpage", SignedOutHeading: label + "-out", SignedOutBody: label + " {app}-outbody", SignIn: label + "-in"}
html := render(t, "estilo-signin", SignIn{Icon: "/i.svg", App: "Docs", Copy: c, Action: "/signin"})
for _, want := range []string{"<h1>" + esc(label) + "-h</h1>", esc(label) + "-addr", esc(label) + "-help", esc(label) + "-submit"} {
if !strings.Contains(html, want) {
@@ -143,3 +144,51 @@ func TestDoorCopyIsReplaceablePerLocale(t *testing.T) {
})
}
}
+
+// The copy's slots are tokens, {app} and {link}, not Printf verbs: a
+// translation that drops the slot, moves it, or carries a literal "%"
+// renders as written rather than as "%!(EXTRA …)" (Fable on estilo #19,
+// finding 4).
+func TestDoorCopyFillsTokensNotFormats(t *testing.T) {
+ c := English
+ c.SignedOutBody = "100% done: {app} is signed out here."
+ if got := (SignInNotice{App: "Docs", Copy: &c}).SignedOut("/signin").Body; got != "100% done: Docs is signed out here." {
+ t.Errorf("signed-out body %q", got)
+ }
+ c.SignedOutBody = "Signed out."
+ if got := (SignInNotice{App: "Docs", Copy: &c}).SignedOut("/signin").Body; got != "Signed out." {
+ t.Errorf("a body without {app} rendered %q", got)
+ }
+ // With no app, the suite is named, as the hub's own page does.
+ if got := (SignInNotice{}).SignedOut("/signin").Body; got != "You’ve signed out of Oficina on this device." {
+ t.Errorf("no-app signed-out body %q", got)
+ }
+ // The link is substituted once: a link that happens to carry "{app}"
+ // is not rewritten.
+ if _, body := English.SignInEmail("Docs", "https://x.example/?q={app}"); !strings.Contains(body, "\n\nhttps://x.example/?q={app}\n\n") {
+ t.Errorf("the link was rewritten: %q", body)
+ }
+}
+
+// The email is part of the copy set: a locale's subject and body are what
+// a locale's email says (Fable on estilo #19, finding 3), and the package
+// helpers are English's.
+func TestSignInEmailReadsTheCopy(t *testing.T) {
+ c := English
+ c.EmailSubject = "Entra en {app}"
+ c.EmailBody = "{app}: {link} (100%)"
+ subject, body := c.SignInEmail("Docs", "L")
+ if subject != "Entra en Docs" || body != "Docs: L (100%)" {
+ t.Errorf("locale email = %q, %q", subject, body)
+ }
+ if got := c.MailSubject("Docs"); got != "Entra en Docs" {
+ t.Errorf("MailSubject %q", got)
+ }
+ if got := c.MailBody("Docs")("L"); got != "Docs: L (100%)" {
+ t.Errorf("MailBody %q", got)
+ }
+ s, b := SignInEmail("Docs", "L")
+ if es, eb := English.SignInEmail("Docs", "L"); s != es || b != eb || EmailSubject("Docs") != es || EmailBody("Docs")("L") != eb {
+ t.Error("the package helpers are not English's email")
+ }
+}