Merge branch 'fix/door-copy-nits' into main
Merge fix/door-copy-nits: {app}/{link} tokens, DoorCopy mail methods, spec citation, translations note
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2 files changed,
+116
−33
chrome/door.go+61 −27chrome/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.003s ok amadan.net/oficina/estilo.git/chrome 0.210s ? amadan.net/oficina/estilo.git/cmd/oficina-notices [no test files] ok amadan.net/oficina/estilo.git/credits 0.006s ok amadan.net/oficina/estilo.git/credits/catalog 0.005s ok amadan.net/oficina/estilo.git/homeconnect 0.005s ok amadan.net/oficina/estilo.git/internal/lint 0.004s ? 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.071s ok amadan.net/oficina/estilo.git/sky 0.022s ok amadan.net/oficina/estilo.git/theme 0.006s
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") |
| + } |
| +} |