Skip to content

Commit d136399

Browse files
committed
ci: apply stable collaboration i18n cleanup
1 parent 1e68024 commit d136399

1 file changed

Lines changed: 115 additions & 0 deletions

File tree

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
name: Apply collaboration i18n cleanup
2+
3+
on:
4+
pull_request:
5+
branches: ["main"]
6+
7+
permissions:
8+
contents: write
9+
10+
jobs:
11+
apply:
12+
if: github.event.pull_request.head.repo.full_name == github.repository && github.head_ref == 'feat/stable-list-collaboration'
13+
runs-on: ubuntu-latest
14+
steps:
15+
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
16+
with:
17+
ref: ${{ github.event.pull_request.head.sha }}
18+
persist-credentials: true
19+
20+
- name: Apply i18n cleanup
21+
shell: bash
22+
run: |
23+
set -euo pipefail
24+
python <<'PY'
25+
from pathlib import Path
26+
27+
def replace_all(path, old, new, minimum=1):
28+
p = Path(path)
29+
text = p.read_text()
30+
count = text.count(old)
31+
if count < minimum:
32+
raise RuntimeError(f"{path}: expected >= {minimum} matches, found {count}: {old[:90]!r}")
33+
p.write_text(text.replace(old, new))
34+
return count
35+
36+
modal = "apps/web/components/dashboard/lists/ManageCollaboratorsModal.tsx"
37+
replace_all(modal, '''import {
38+
canManageCollaboratorOnList,
39+
collaboratorRemovalMessage,
40+
formatInvitationDate,
41+
invitationDeliveryMessage,
42+
} from "./collaborationUi";''', '''import {
43+
canManageCollaboratorOnList,
44+
formatInvitationDate,
45+
} from "./collaborationUi";''')
46+
count = replace_all(
47+
modal,
48+
'toast({ description: invitationDeliveryMessage(result.emailSent) });',
49+
'''toast({
50+
description: t(
51+
result.emailSent
52+
? "lists.collaborators.invitation_delivery_sent"
53+
: "lists.collaborators.invitation_delivery_failed",
54+
),
55+
});''',
56+
minimum=2,
57+
)
58+
if count != 2:
59+
raise RuntimeError(f"expected exactly 2 invitation delivery toasts, found {count}")
60+
61+
replacements = [
62+
('"Invite people to this list and optionally include current and future nested lists."', 't("lists.collaborators.stable_description")'),
63+
('aria-label="Invitation role"', 'aria-label={t("lists.collaborators.invitation_role")}'),
64+
('<SelectItem value="viewer">Viewer</SelectItem>', '<SelectItem value="viewer">{t("lists.collaborators.viewer")}</SelectItem>'),
65+
('<SelectItem value="editor">Editor</SelectItem>', '<SelectItem value="editor">{t("lists.collaborators.editor")}</SelectItem>'),
66+
(' Invite\n', ' {t("lists.collaborators.invite")}\n'),
67+
(' Also share all nested lists\n', ' {t("lists.collaborators.share_all_nested")}\n'),
68+
(' Includes current nested lists and lists added or moved here\n later. Leave this off to share only this list.\n', ' {t("lists.collaborators.share_all_nested_description")}\n'),
69+
('<span className="text-sm text-muted-foreground">Owner</span>', '<span className="text-sm text-muted-foreground">\n {t("lists.collaborators.owner")}\n </span>'),
70+
('{expired ? "Expired" : "Pending"}', '{expired\n ? t("lists.collaborators.expired")\n : t("lists.collaborators.pending")}'),
71+
('<Badge variant="secondary">Inherited</Badge>', '<Badge variant="secondary">\n {t("lists.collaborators.inherited")}\n </Badge>'),
72+
('<Badge variant="outline">Nested lists</Badge>', '<Badge variant="outline">\n {t("lists.collaborators.nested_lists")}\n </Badge>'),
73+
(' Inherited from {collaborator.sourceListName}\n', ' {t("lists.collaborators.inherited_from", {\n name: collaborator.sourceListName,\n })}\n'),
74+
('{expired ? "Expired" : "Expires"}{" "}', '{expired\n ? t("lists.collaborators.expired")\n : t("lists.collaborators.expires")}{" "}'),
75+
('{collaborator.role}', '{t(\n collaborator.role === "viewer"\n ? "lists.collaborators.viewer"\n : "lists.collaborators.editor",\n )}'),
76+
(' Override here\n', ' {t("lists.collaborators.override_here")}\n'),
77+
('aria-label="Pending invitation role"', 'aria-label={t("lists.collaborators.pending_invitation_role")}'),
78+
(' Nested lists\n', ' {t("lists.collaborators.nested_lists")}\n'),
79+
('<RefreshCw className="mr-1 size-3" /> Resend', '<RefreshCw className="mr-1 size-3" />\n {t("lists.collaborators.resend")}'),
80+
(' Revoke\n', ' {t("lists.collaborators.revoke")}\n'),
81+
('aria-label="Collaborator role"', 'aria-label={t("lists.collaborators.collaborator_role")}'),
82+
('aria-label={`Remove ${collaborator.user.name}`}', 'aria-label={t("lists.collaborators.remove_aria", {\n name: collaborator.user.name,\n })}'),
83+
(' collaboratorRemovalMessage(\n collaborator.user.name,\n ),', ' t("lists.collaborators.remove_confirmation", {\n name: collaborator.user.name,\n }),'),
84+
]
85+
for old, new in replacements:
86+
replace_all(modal, old, new)
87+
88+
pending = "apps/web/components/dashboard/lists/PendingInvitationsCard.tsx"
89+
for old, new in [
90+
(' {invitation.role}\n', ' {t(\n invitation.role === "viewer"\n ? "lists.collaborators.viewer"\n : "lists.collaborators.editor",\n )}\n'),
91+
('<Badge variant="secondary">Includes nested lists</Badge>', '<Badge variant="secondary">\n {t("lists.invitations.includes_nested_lists")}\n </Badge>'),
92+
('<Badge variant="destructive">Expired</Badge>', '<Badge variant="destructive">\n {t("lists.invitations.expired")}\n </Badge>'),
93+
('{invitation.list.owner?.name || "Unknown"}', '{invitation.list.owner?.name || t("lists.invitations.unknown")}'),
94+
('{invitation.expired ? "Expired" : "Expires"}{" "}', '{invitation.expired\n ? t("lists.invitations.expired")\n : t("lists.invitations.expires")}{" "}'),
95+
(' Ask the list owner to resend this invitation to renew it for 30\n days.\n', ' {t("lists.invitations.expired_help")}\n'),
96+
]:
97+
replace_all(pending, old, new)
98+
99+
Path("apps/web/components/dashboard/lists/collaborationUi.ts").write_text('''const invitationDateFormatter = new Intl.DateTimeFormat("en-US", {\n dateStyle: "medium",\n timeZone: "UTC",\n});\n\nexport function formatInvitationDate(date: Date) {\n return invitationDateFormatter.format(date);\n}\n\nexport function canManageCollaboratorOnList(collaborator: {\n status: "pending" | "accepted" | "declined";\n inherited?: boolean;\n}) {\n return collaborator.status === "accepted" && !collaborator.inherited;\n}\n''')
100+
Path("apps/web/components/dashboard/lists/collaborationUi.test.ts").write_text('''import { describe, expect, test } from "vitest";\n\nimport {\n canManageCollaboratorOnList,\n formatInvitationDate,\n} from "./collaborationUi";\n\ndescribe("stable collaboration UI semantics", () => {\n test("formats invitation dates deterministically", () => {\n expect(formatInvitationDate(new Date("2026-08-15T23:30:00-07:00"))).toBe(\n "Aug 16, 2026",\n );\n });\n\n test("only direct accepted collaborators can be managed from this list", () => {\n expect(\n canManageCollaboratorOnList({ status: "accepted", inherited: false }),\n ).toBe(true);\n expect(\n canManageCollaboratorOnList({ status: "accepted", inherited: true }),\n ).toBe(false);\n expect(\n canManageCollaboratorOnList({ status: "pending", inherited: false }),\n ).toBe(false);\n });\n});\n''')
101+
102+
collaborator_keys = ''' "stable_description": "Invite people to this list and optionally include current and future nested lists.",\n "invitation_role": "Invitation role",\n "invite": "Invite",\n "share_all_nested": "Also share all nested lists",\n "share_all_nested_description": "Includes current nested lists and lists added or moved here later. Leave this off to share only this list.",\n "nested_lists": "Nested lists",\n "inherited": "Inherited",\n "inherited_from": "Inherited from {{name}}",\n "expired": "Expired",\n "expires": "Expires",\n "override_here": "Override here",\n "pending_invitation_role": "Pending invitation role",\n "collaborator_role": "Collaborator role",\n "resend": "Resend",\n "invitation_delivery_sent": "Invitation created and email sent.",\n "invitation_delivery_failed": "Invitation created, but the email was not sent. You can resend it later.",\n "remove_aria": "Remove {{name}}",\n "remove_confirmation": "Remove {{name}} from this shared list? Bookmark entries they contributed through this collaboration will be removed from this shared list, but their underlying bookmarks will remain in their library.",\n'''
103+
invitation_keys = ''' "includes_nested_lists": "Includes nested lists",\n "expired": "Expired",\n "expires": "Expires",\n "unknown": "Unknown",\n "expired_help": "Ask the list owner to resend this invitation to renew it for 30 days.",\n'''
104+
for locale in [
105+
"apps/web/lib/i18n/locales/en/translation.json",
106+
"apps/web/lib/i18n/locales/en_US/translation.json",
107+
]:
108+
replace_all(locale, ' "manage": "Manage Collaborators",\n', ' "manage": "Manage Collaborators",\n' + collaborator_keys)
109+
replace_all(locale, ' "pending": "Pending Invitations",\n', ' "pending": "Pending Invitations",\n' + invitation_keys)
110+
PY
111+
git config user.name "github-actions[bot]"
112+
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
113+
git add apps/web/components/dashboard/lists apps/web/lib/i18n/locales/en/translation.json apps/web/lib/i18n/locales/en_US/translation.json
114+
git commit --no-verify -m "fix(web): localize stable collaboration UI"
115+
git push origin HEAD:${{ github.head_ref }}

0 commit comments

Comments
 (0)