diff --git a/packages/admin/src/components/AdminCommandPalette.tsx b/packages/admin/src/components/AdminCommandPalette.tsx index 3ccf5b7fc..5d9400726 100644 --- a/packages/admin/src/components/AdminCommandPalette.tsx +++ b/packages/admin/src/components/AdminCommandPalette.tsx @@ -6,6 +6,9 @@ */ import { CommandPalette } from "@cloudflare/kumo"; +import type { MessageDescriptor } from "@lingui/core"; +import { msg } from "@lingui/core/macro"; +import { useLingui } from "@lingui/react/macro"; import { SquaresFour, FileText, @@ -25,9 +28,15 @@ import { useNavigate } from "@tanstack/react-router"; import * as React from "react"; import { useHotkeys } from "react-hotkeys-hook"; -import { apiFetch } from "../lib/api/client"; +import { apiFetch, type AdminManifest } from "../lib/api/client.js"; import { useCurrentUser } from "../lib/api/current-user"; +/** Subset of manifest fields used by the palette (matches `Shell` props shape). */ +type CommandPaletteManifest = { + collections: Record; + plugins: AdminManifest["plugins"]; +}; + // Role levels (matching @emdash-cms/auth) const ROLE_ADMIN = 50; const ROLE_EDITOR = 40; @@ -75,7 +84,7 @@ interface SearchResponse { interface NavItem { id: string; - title: string; + title: string | MessageDescriptor; to: string; params?: Record; icon: React.ElementType; @@ -84,7 +93,8 @@ interface NavItem { } interface ResultGroup { - label: string; + id: string; + label: MessageDescriptor; items: ResultItem[]; } @@ -99,20 +109,7 @@ interface ResultItem { } interface AdminCommandPaletteProps { - manifest: { - collections: Record; - plugins: Record< - string, - { - package?: string; - enabled?: boolean; - adminPages?: Array<{ - path: string; - label?: string; - }>; - } - >; - }; + manifest: CommandPaletteManifest; } async function searchContent(query: string): Promise { @@ -127,14 +124,11 @@ async function searchContent(query: string): Promise { return body.data; } -function buildNavItems( - manifest: AdminCommandPaletteProps["manifest"], - userRole: number, -): NavItem[] { +function buildNavItems(manifest: CommandPaletteManifest, userRole: number): NavItem[] { const items: NavItem[] = [ { id: "dashboard", - title: "Dashboard", + title: msg`Dashboard`, to: "/", icon: SquaresFour, keywords: ["home", "overview"], @@ -157,14 +151,14 @@ function buildNavItems( items.push( { id: "media", - title: "Media Library", + title: msg`Media Library`, to: "/media", icon: Image, keywords: ["images", "files", "uploads"], }, { id: "menus", - title: "Menus", + title: msg`Menus`, to: "/menus", icon: List, minRole: ROLE_EDITOR, @@ -172,7 +166,7 @@ function buildNavItems( }, { id: "widgets", - title: "Widgets", + title: msg`Widgets`, to: "/widgets", icon: GridFour, minRole: ROLE_EDITOR, @@ -180,7 +174,7 @@ function buildNavItems( }, { id: "sections", - title: "Sections", + title: msg`Sections`, to: "/sections", icon: Stack, minRole: ROLE_EDITOR, @@ -188,7 +182,7 @@ function buildNavItems( }, { id: "content-types", - title: "Content Types", + title: msg`Content Types`, to: "/content-types", icon: Database, minRole: ROLE_ADMIN, @@ -196,7 +190,7 @@ function buildNavItems( }, { id: "categories", - title: "Categories", + title: msg`Categories`, to: "/taxonomies/$taxonomy", params: { taxonomy: "category" }, icon: FileText, @@ -205,7 +199,7 @@ function buildNavItems( }, { id: "tags", - title: "Tags", + title: msg`Tags`, to: "/taxonomies/$taxonomy", params: { taxonomy: "tag" }, icon: FileText, @@ -214,7 +208,7 @@ function buildNavItems( }, { id: "users", - title: "Users", + title: msg`Users`, to: "/users", icon: Users, minRole: ROLE_ADMIN, @@ -222,7 +216,7 @@ function buildNavItems( }, { id: "plugins", - title: "Plugins", + title: msg`Plugins`, to: "/plugins-manager", icon: PuzzlePiece, minRole: ROLE_ADMIN, @@ -230,7 +224,7 @@ function buildNavItems( }, { id: "import", - title: "Import", + title: msg`Import`, to: "/import/wordpress", icon: Upload, minRole: ROLE_ADMIN, @@ -238,7 +232,7 @@ function buildNavItems( }, { id: "settings", - title: "Settings", + title: msg`Settings`, to: "/settings", icon: Gear, minRole: ROLE_ADMIN, @@ -246,7 +240,7 @@ function buildNavItems( }, { id: "security", - title: "Security Settings", + title: msg`Security Settings`, to: "/settings/security", icon: Gear, minRole: ROLE_ADMIN, @@ -281,17 +275,23 @@ function buildNavItems( return items.filter((item) => !item.minRole || userRole >= item.minRole); } -function filterNavItems(items: NavItem[], query: string): NavItem[] { +function filterNavItems( + items: NavItem[], + query: string, + translate: (d: MessageDescriptor) => string, +): NavItem[] { if (!query) return items; const lowerQuery = query.toLowerCase(); return items.filter((item) => { - const titleMatch = item.title.toLowerCase().includes(lowerQuery); + const titleStr = typeof item.title === "string" ? item.title : translate(item.title); + const titleMatch = titleStr.toLowerCase().includes(lowerQuery); const keywordMatch = item.keywords?.some((k) => k.toLowerCase().includes(lowerQuery)); return titleMatch || keywordMatch; }); } export function AdminCommandPalette({ manifest }: AdminCommandPaletteProps) { + const { t } = useLingui(); const [open, setOpen] = React.useState(false); const [query, setQuery] = React.useState(""); const navigate = useNavigate(); @@ -320,8 +320,8 @@ export function AdminCommandPalette({ manifest }: AdminCommandPaletteProps) { // Filter nav items based on query const filteredNavItems = React.useMemo( - () => filterNavItems(allNavItems, query), - [allNavItems, query], + () => filterNavItems(allNavItems, query, t), + [allNavItems, query, t], ); // Build result groups @@ -331,10 +331,11 @@ export function AdminCommandPalette({ manifest }: AdminCommandPaletteProps) { // Navigation group if (filteredNavItems.length > 0) { groups.push({ - label: "Navigation", + id: "navigation", + label: msg`Navigation`, items: filteredNavItems.map((item) => ({ id: item.id, - title: item.title, + title: typeof item.title === "string" ? item.title : t(item.title), to: item.to, params: item.params, icon: , @@ -346,25 +347,28 @@ export function AdminCommandPalette({ manifest }: AdminCommandPaletteProps) { if (searchResults?.items && searchResults.items.length > 0) { const contentItems = searchResults.items.map((result) => { const collectionConfig = manifest.collections[result.collection]; + const collectionLabel = collectionConfig?.label ?? result.collection; + return { id: `content-${result.id}`, title: result.title || result.slug, to: "/content/$collection/$id", params: { collection: result.collection, id: result.id }, icon: , - description: collectionConfig?.label || result.collection, + description: collectionLabel, collection: result.collection, }; }); groups.push({ - label: "Content", + id: "content", + label: msg`Content`, items: contentItems, }); } return groups; - }, [filteredNavItems, searchResults, manifest.collections]); + }, [filteredNavItems, searchResults, manifest.collections, t]); // Keyboard shortcut to open (Cmd+K / Ctrl+K) useHotkeys("mod+k", (e) => { @@ -413,12 +417,12 @@ export function AdminCommandPalette({ manifest }: AdminCommandPaletteProps) { items={resultGroups} value={query} onValueChange={setQuery} - itemToStringValue={(group) => group.label} + itemToStringValue={(group) => t(group.label)} onSelect={handleSelect} getSelectableItems={(groups) => groups.flatMap((g) => g.items)} > } /> @@ -428,8 +432,8 @@ export function AdminCommandPalette({ manifest }: AdminCommandPaletteProps) { <> {(group: ResultGroup) => ( - - {group.label} + + {t(group.label)} {(item: ResultItem) => ( )} - No results found + {t`No results found`} )} @@ -453,17 +457,17 @@ export function AdminCommandPalette({ manifest }: AdminCommandPaletteProps) {
Enter - to select + {t`to select`} {IS_MAC ? "Cmd" : "Ctrl"}+Enter - new tab + {t`new tab`} Esc - to close + {t`to close`}
diff --git a/packages/admin/src/components/ContentTypeEditor.tsx b/packages/admin/src/components/ContentTypeEditor.tsx index d020ad9eb..dd7205be0 100644 --- a/packages/admin/src/components/ContentTypeEditor.tsx +++ b/packages/admin/src/components/ContentTypeEditor.tsx @@ -16,6 +16,9 @@ import { verticalListSortingStrategy, } from "@dnd-kit/sortable"; import { CSS } from "@dnd-kit/utilities"; +import type { MessageDescriptor } from "@lingui/core"; +import { msg } from "@lingui/core/macro"; +import { useLingui } from "@lingui/react/macro"; import { ArrowLeft, Plus, @@ -54,26 +57,32 @@ export interface ContentTypeEditorProps { onReorderFields?: (fieldSlugs: string[]) => void; } -const SUPPORT_OPTIONS = [ +interface SupportOptionDef { + value: string; + label: MessageDescriptor; + description: MessageDescriptor; +} + +const SUPPORT_OPTIONS: SupportOptionDef[] = [ { value: "drafts", - label: "Drafts", - description: "Save content as draft before publishing", + label: msg`Drafts`, + description: msg`Save content as draft before publishing`, }, { value: "revisions", - label: "Revisions", - description: "Track content history", + label: msg`Revisions`, + description: msg`Track content history`, }, { value: "preview", - label: "Preview", - description: "Preview content before publishing", + label: msg`Preview`, + description: msg`Preview content before publishing`, }, { value: "search", - label: "Search", - description: "Enable full-text search on this collection", + label: msg`Search`, + description: msg`Enable full-text search on this collection`, }, ]; @@ -81,42 +90,49 @@ const SUPPORT_OPTIONS = [ * System fields that exist on every collection * These are created automatically and cannot be modified */ -const SYSTEM_FIELDS = [ +interface SystemFieldDef { + slug: string; + label: MessageDescriptor; + type: string; + description: MessageDescriptor; +} + +const SYSTEM_FIELDS: SystemFieldDef[] = [ { slug: "id", - label: "ID", + label: msg`ID`, type: "text", - description: "Unique identifier (ULID)", + description: msg`Unique identifier (ULID)`, }, { slug: "slug", - label: "Slug", + label: msg`Slug`, type: "text", - description: "URL-friendly identifier", + description: msg`URL-friendly identifier`, }, { slug: "status", - label: "Status", + label: msg`Status`, type: "text", - description: "draft, published, or archived", + description: msg`draft, published, or archived`, }, { slug: "created_at", - label: "Created At", + label: msg`Created At`, type: "datetime", - description: "When the entry was created", + description: msg`When the entry was created`, }, { slug: "updated_at", - label: "Updated At", + label: msg`Updated At`, type: "datetime", - description: "When the entry was last modified", + description: msg`When the entry was last modified`, }, { slug: "published_at", - label: "Published At", + label: msg`Published At`, type: "datetime", - description: "When the entry was published", + description: msg`When the entry was published`, }, ]; @@ -133,6 +149,7 @@ export function ContentTypeEditor({ onDeleteField, onReorderFields, }: ContentTypeEditorProps) { + const { t } = useLingui(); const _navigate = useNavigate(); // Form state @@ -417,8 +434,8 @@ export function ContentTypeEditor({ disabled={isFromCode} />
- {option.label} -

{option.description}

+ {t(option.label)} +

{t(option.description)}

))} @@ -728,24 +745,25 @@ function FieldRow({ field, isFromCode, onEdit, onDelete }: FieldRowProps) { interface SystemFieldInfo { slug: string; - label: string; + label: MessageDescriptor; type: string; - description: string; + description: MessageDescriptor; } function SystemFieldRow({ field }: { field: SystemFieldInfo }) { + const { t } = useLingui(); return (
{/* Spacer for alignment with draggable fields */}
- {field.label} + {t(field.label)} {field.slug} System
-

{field.description}

+

{t(field.description)}

); diff --git a/packages/admin/src/components/PortableTextEditor.tsx b/packages/admin/src/components/PortableTextEditor.tsx index d1e0f55b6..7826fda0b 100644 --- a/packages/admin/src/components/PortableTextEditor.tsx +++ b/packages/admin/src/components/PortableTextEditor.tsx @@ -14,6 +14,9 @@ import { Button, Dialog, Input } from "@cloudflare/kumo"; import type { Element } from "@emdash-cms/blocks"; import { useFloating, offset, flip, shift, autoUpdate } from "@floating-ui/react"; +import type { MessageDescriptor } from "@lingui/core"; +import { msg } from "@lingui/core/macro"; +import { useLingui } from "@lingui/react/macro"; import { TextB, TextItalic, @@ -678,12 +681,13 @@ function convertPTMarks(marks: string[], markDefs: Map; command: (props: { editor: Editor; range: Range }) => void; aliases?: string[]; - category?: string; + category?: MessageDescriptor; } /** @@ -692,8 +696,8 @@ interface SlashCommandItem { const defaultSlashCommands: SlashCommandItem[] = [ { id: "heading1", - title: "Heading 1", - description: "Large section heading", + title: msg`Heading 1`, + description: msg`Large section heading`, icon: TextHOne, aliases: ["h1", "title"], command: ({ editor, range }) => { @@ -702,8 +706,8 @@ const defaultSlashCommands: SlashCommandItem[] = [ }, { id: "heading2", - title: "Heading 2", - description: "Medium section heading", + title: msg`Heading 2`, + description: msg`Medium section heading`, icon: TextHTwo, aliases: ["h2", "subtitle"], command: ({ editor, range }) => { @@ -712,8 +716,8 @@ const defaultSlashCommands: SlashCommandItem[] = [ }, { id: "heading3", - title: "Heading 3", - description: "Small section heading", + title: msg`Heading 3`, + description: msg`Small section heading`, icon: TextHThree, aliases: ["h3"], command: ({ editor, range }) => { @@ -722,8 +726,8 @@ const defaultSlashCommands: SlashCommandItem[] = [ }, { id: "bulletList", - title: "Bullet List", - description: "Create a bullet list", + title: msg`Bullet List`, + description: msg`Create a bullet list`, icon: List, aliases: ["ul", "unordered"], command: ({ editor, range }) => { @@ -732,8 +736,8 @@ const defaultSlashCommands: SlashCommandItem[] = [ }, { id: "numberedList", - title: "Numbered List", - description: "Create a numbered list", + title: msg`Numbered List`, + description: msg`Create a numbered list`, icon: ListNumbers, aliases: ["ol", "ordered"], command: ({ editor, range }) => { @@ -742,8 +746,8 @@ const defaultSlashCommands: SlashCommandItem[] = [ }, { id: "quote", - title: "Quote", - description: "Insert a blockquote", + title: msg`Quote`, + description: msg`Insert a blockquote`, icon: Quotes, aliases: ["blockquote", "cite"], command: ({ editor, range }) => { @@ -752,8 +756,8 @@ const defaultSlashCommands: SlashCommandItem[] = [ }, { id: "codeBlock", - title: "Code Block", - description: "Insert a code block", + title: msg`Code Block`, + description: msg`Insert a code block`, icon: CodeBlock, aliases: ["code", "pre", "```"], command: ({ editor, range }) => { @@ -762,8 +766,8 @@ const defaultSlashCommands: SlashCommandItem[] = [ }, { id: "divider", - title: "Divider", - description: "Insert a horizontal rule", + title: msg`Divider`, + description: msg`Insert a horizontal rule`, icon: Minus, aliases: ["hr", "---", "separator"], command: ({ editor, range }) => { @@ -889,6 +893,7 @@ function SlashCommandMenu({ onClose: () => void; setSelectedIndex: (index: number) => void; }) { + const { t } = useLingui(); const containerRef = React.useRef(null); const { refs, floatingStyles } = useFloating({ @@ -932,7 +937,7 @@ function SlashCommandMenu({ className="z-[100] rounded-lg border bg-kumo-overlay p-1 shadow-lg min-w-[220px] max-h-[300px] overflow-y-auto" > {state.items.length === 0 ? ( -

No results

+

{t`No results`}

) : ( state.items.map((item, index) => ( )) @@ -1346,6 +1355,8 @@ export function PortableTextEditor({ onBlockSidebarOpen, onBlockSidebarClose, }: PortableTextEditorProps) { + const { t } = useLingui(); + // Use a ref for onChange to avoid recreating the editor when the callback changes const onChangeRef = React.useRef(onChange); React.useEffect(() => { @@ -1399,11 +1410,11 @@ export function PortableTextEditor({ // Add image command cmds.push({ id: "image", - title: "Image", - description: "Insert an image", + title: msg`Image`, + description: msg`Insert an image`, icon: ImageIcon, aliases: ["img", "photo", "picture", "url"], - category: "Media", + category: msg`Media`, command: ({ editor, range }) => { editor.chain().focus().deleteRange(range).run(); setMediaPickerOpen(true); @@ -1413,26 +1424,26 @@ export function PortableTextEditor({ // Add section command cmds.push({ id: "section", - title: "Section", - description: "Insert a reusable section", + title: msg`Section`, + description: msg`Insert a reusable section`, icon: Stack, aliases: ["pattern", "block", "template"], - category: "Content", + category: msg`Content`, command: ({ editor, range }) => { editor.chain().focus().deleteRange(range).run(); setSectionPickerOpen(true); }, }); - // Add plugin block commands + // Add plugin block commands (API labels/descriptions: plain strings, not msg-wrapped) for (const block of pluginBlocks) { cmds.push({ id: `plugin-${block.pluginId}-${block.type}`, title: block.label, - description: block.description || `Embed a ${block.label.toLowerCase()}`, + description: block.description ?? t(msg`Embed a ${block.label}`), icon: resolveIcon(block.icon), aliases: [block.type], - category: "Embeds", + category: msg`Embeds`, command: ({ editor, range }) => { editor.chain().focus().deleteRange(range).run(); setPluginBlockModal(block); @@ -1441,7 +1452,7 @@ export function PortableTextEditor({ } return cmds; - }, [pluginBlocks]); + }, [pluginBlocks, t]); // Filter commands by query — accessed via ref so the Suggestion plugin // (created once) always sees the latest command list without needing @@ -1453,10 +1464,12 @@ export function PortableTextEditor({ const titleMatches: SlashCommandItem[] = []; const otherMatches: SlashCommandItem[] = []; for (const item of slashCommands) { - if (item.title.toLowerCase().includes(searchText)) { + const titleStr = typeof item.title === "string" ? item.title : t(item.title); + const descStr = typeof item.description === "string" ? item.description : t(item.description); + if (titleStr.toLowerCase().includes(searchText)) { titleMatches.push(item); } else if ( - item.description.toLowerCase().includes(searchText) || + descStr.toLowerCase().includes(searchText) || item.aliases?.some((alias) => alias.toLowerCase().includes(searchText)) ) { otherMatches.push(item); diff --git a/packages/admin/src/components/WelcomeModal.tsx b/packages/admin/src/components/WelcomeModal.tsx index 85b62ce4f..db23e0bee 100644 --- a/packages/admin/src/components/WelcomeModal.tsx +++ b/packages/admin/src/components/WelcomeModal.tsx @@ -5,6 +5,9 @@ */ import { Button, Dialog } from "@cloudflare/kumo"; +import type { MessageDescriptor } from "@lingui/core"; +import { msg } from "@lingui/core/macro"; +import { useLingui } from "@lingui/react/macro"; import { X } from "@phosphor-icons/react"; import { useMutation, useQueryClient } from "@tanstack/react-query"; import * as React from "react"; @@ -19,15 +22,37 @@ interface WelcomeModalProps { userRole: number; } -// Role labels -function getRoleLabel(role: number): string { - if (role >= 50) return "Administrator"; - if (role >= 40) return "Editor"; - if (role >= 30) return "Author"; - if (role >= 20) return "Contributor"; - return "Subscriber"; +const MSG_ROLE_ADMINISTRATOR = msg`Administrator`; +const MSG_ROLE_EDITOR = msg`Editor`; +const MSG_ROLE_AUTHOR = msg`Author`; +const MSG_ROLE_CONTRIBUTOR = msg`Contributor`; +const MSG_ROLE_SUBSCRIBER = msg`Subscriber`; + +function roleDescriptor(role: number): MessageDescriptor { + if (role >= 50) return MSG_ROLE_ADMINISTRATOR; + if (role >= 40) return MSG_ROLE_EDITOR; + if (role >= 30) return MSG_ROLE_AUTHOR; + if (role >= 20) return MSG_ROLE_CONTRIBUTOR; + return MSG_ROLE_SUBSCRIBER; +} + +const MSG_ACCOUNT_CREATED = msg`Your account has been created successfully.`; +const MSG_YOUR_ROLE = msg`Your Role`; +const MSG_SCOPE_ADMIN = msg`You have full access to manage this site, including users, settings, and all content.`; +const MSG_SCOPE_EDITOR = msg`You can manage content, media, menus, and taxonomies.`; +const MSG_SCOPE_AUTHOR = msg`You can create and edit your own content.`; +const MSG_SCOPE_CONTRIBUTOR = msg`You can view and contribute to the site.`; + +function scopeDescriptor(isAdmin: boolean, userRole: number): MessageDescriptor { + if (isAdmin) return MSG_SCOPE_ADMIN; + if (userRole >= 40) return MSG_SCOPE_EDITOR; + if (userRole >= 30) return MSG_SCOPE_AUTHOR; + return MSG_SCOPE_CONTRIBUTOR; } +const MSG_ADMIN_INVITE = msg`As an administrator, you can invite other users from the Users section.`; +const MSG_CLOSE = msg`Close`; + async function dismissWelcome(): Promise { const response = await apiFetch("/_emdash/api/auth/me", { method: "POST", @@ -38,6 +63,7 @@ async function dismissWelcome(): Promise { } export function WelcomeModal({ open, onClose, userName, userRole }: WelcomeModalProps) { + const { t } = useLingui(); const queryClient = useQueryClient(); const dismissMutation = useMutation({ @@ -62,26 +88,30 @@ export function WelcomeModal({ open, onClose, userName, userRole }: WelcomeModal dismissMutation.mutate(); }; - const roleLabel = getRoleLabel(userRole); + const roleLabel = t(roleDescriptor(userRole)); const isAdmin = userRole >= 50; + const firstName = userName?.split(" ")?.[0]?.trim() ?? ""; + const titleDescriptor = + firstName.length > 0 ? msg`Welcome to EmDash, ${firstName}!` : msg`Welcome to EmDash!`; + return ( !isOpen && handleGetStarted()}>
( )} /> @@ -91,39 +121,26 @@ export function WelcomeModal({ open, onClose, userName, userRole }: WelcomeModal
- Welcome to EmDash{userName ? `, ${userName.split(" ")[0]}` : ""}! + {t(titleDescriptor)} - Your account has been created successfully. + {t(MSG_ACCOUNT_CREATED)}
-
Your Role
+
{t(MSG_YOUR_ROLE)}
{roleLabel}
-

- {isAdmin - ? "You have full access to manage this site, including users, settings, and all content." - : userRole >= 40 - ? "You can manage content, media, menus, and taxonomies." - : userRole >= 30 - ? "You can create and edit your own content." - : "You can view and contribute to the site."} -

+

{t(scopeDescriptor(isAdmin, userRole))}

- {isAdmin && ( -

- As an administrator, you can invite other users from the{" "} - Users section. -

- )} + {isAdmin &&

{t(MSG_ADMIN_INVITE)}

}
diff --git a/packages/admin/src/components/Widgets.tsx b/packages/admin/src/components/Widgets.tsx index 2ed6a579b..e35b4631e 100644 --- a/packages/admin/src/components/Widgets.tsx +++ b/packages/admin/src/components/Widgets.tsx @@ -29,6 +29,9 @@ import { useSortable, } from "@dnd-kit/sortable"; import { CSS } from "@dnd-kit/utilities"; +import type { MessageDescriptor } from "@lingui/core"; +import { msg } from "@lingui/core/macro"; +import { useLingui } from "@lingui/react/macro"; import { Plus, DotsSixVertical, Trash, CaretDown, CaretRight } from "@phosphor-icons/react"; import { X } from "@phosphor-icons/react"; import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; @@ -76,25 +79,26 @@ function isPaletteItem(data: DragItemData): data is PaletteItemData { /** Built-in widget types available in the palette */ const BUILTIN_WIDGETS: Array<{ id: string; - label: string; - description: string; + label: MessageDescriptor; + description: MessageDescriptor; input: CreateWidgetInput; }> = [ { id: "palette-content", - label: "Content Block", - description: "Rich text content", - input: { type: "content", title: "Content Block" }, + label: msg`Content Block`, + description: msg`Rich text content`, + input: { type: "content" }, }, { id: "palette-menu", - label: "Menu", - description: "Display a navigation menu", - input: { type: "menu", title: "Menu" }, + label: msg`Menu`, + description: msg`Display a navigation menu`, + input: { type: "menu" }, }, ]; export function Widgets() { + const { t } = useLingui(); const queryClient = useQueryClient(); const toastManager = Toast.useToastManager(); const [isCreateAreaOpen, setIsCreateAreaOpen] = React.useState(false); @@ -370,9 +374,9 @@ export function Widgets() { ))} {components.map((comp) => ( diff --git a/packages/admin/src/components/editor/BlockMenu.tsx b/packages/admin/src/components/editor/BlockMenu.tsx index a0e75623e..e7115dd07 100644 --- a/packages/admin/src/components/editor/BlockMenu.tsx +++ b/packages/admin/src/components/editor/BlockMenu.tsx @@ -12,6 +12,9 @@ import { Button } from "@cloudflare/kumo"; import { useFloating, offset, flip, shift, autoUpdate } from "@floating-ui/react"; +import type { MessageDescriptor } from "@lingui/core"; +import { msg } from "@lingui/core/macro"; +import { useLingui } from "@lingui/react/macro"; import { DotsSixVertical, Paragraph, @@ -39,7 +42,7 @@ import { cn } from "../../lib/utils"; */ interface BlockTransform { id: string; - label: string; + label: MessageDescriptor; icon: PhosphorIcon; transform: (editor: Editor) => void; } @@ -47,7 +50,7 @@ interface BlockTransform { const blockTransforms: BlockTransform[] = [ { id: "paragraph", - label: "Paragraph", + label: msg`Paragraph`, icon: Paragraph, transform: (editor) => { editor.chain().focus().setNode("paragraph").run(); @@ -55,7 +58,7 @@ const blockTransforms: BlockTransform[] = [ }, { id: "heading1", - label: "Heading 1", + label: msg`Heading 1`, icon: TextHOne, transform: (editor) => { editor.chain().focus().setNode("heading", { level: 1 }).run(); @@ -63,7 +66,7 @@ const blockTransforms: BlockTransform[] = [ }, { id: "heading2", - label: "Heading 2", + label: msg`Heading 2`, icon: TextHTwo, transform: (editor) => { editor.chain().focus().setNode("heading", { level: 2 }).run(); @@ -71,7 +74,7 @@ const blockTransforms: BlockTransform[] = [ }, { id: "heading3", - label: "Heading 3", + label: msg`Heading 3`, icon: TextHThree, transform: (editor) => { editor.chain().focus().setNode("heading", { level: 3 }).run(); @@ -79,7 +82,7 @@ const blockTransforms: BlockTransform[] = [ }, { id: "blockquote", - label: "Quote", + label: msg`Quote`, icon: Quotes, transform: (editor) => { editor.chain().focus().toggleBlockquote().run(); @@ -87,7 +90,7 @@ const blockTransforms: BlockTransform[] = [ }, { id: "codeBlock", - label: "Code Block", + label: msg`Code Block`, icon: Code, transform: (editor) => { editor.chain().focus().toggleCodeBlock().run(); @@ -95,7 +98,7 @@ const blockTransforms: BlockTransform[] = [ }, { id: "bulletList", - label: "Bullet List", + label: msg`Bullet List`, icon: List, transform: (editor) => { editor.chain().focus().toggleBulletList().run(); @@ -103,7 +106,7 @@ const blockTransforms: BlockTransform[] = [ }, { id: "orderedList", - label: "Numbered List", + label: msg`Numbered List`, icon: ListNumbers, transform: (editor) => { editor.chain().focus().toggleOrderedList().run(); @@ -125,6 +128,7 @@ interface BlockMenuProps { * Block Menu - floating menu for block-level actions */ export function BlockMenu({ editor, anchorElement, isOpen, onClose }: BlockMenuProps) { + const { t } = useLingui(); const [showTransforms, setShowTransforms] = React.useState(false); const menuRef = React.useRef(null); const stableOnClose = useStableCallback(onClose); @@ -258,7 +262,7 @@ export function BlockMenu({ editor, anchorElement, isOpen, onClose }: BlockMenuP onClick={() => handleTransform(transform)} > - {transform.label} + {t(transform.label)} ))}
diff --git a/packages/admin/src/components/settings/AllowedDomainsSettings.tsx b/packages/admin/src/components/settings/AllowedDomainsSettings.tsx index 22bc6e2a9..79fc48255 100644 --- a/packages/admin/src/components/settings/AllowedDomainsSettings.tsx +++ b/packages/admin/src/components/settings/AllowedDomainsSettings.tsx @@ -29,19 +29,10 @@ import { fetchManifest, type AllowedDomain, } from "../../lib/api"; - -const ROLES = [ - { value: 10, label: "Subscriber" }, - { value: 20, label: "Contributor" }, - { value: 30, label: "Author" }, - { value: 40, label: "Editor" }, -] as const; - -function getRoleName(level: number): string { - return ROLES.find((r) => r.value === level)?.label ?? "Unknown"; -} +import { useAllowedDomainsRolesConfig } from "./useAllowedDomainsRolesConfig.js"; export function AllowedDomainsSettings() { + const { getRoleLabel, signupRoles, signupRoleItems } = useAllowedDomainsRolesConfig(); const queryClient = useQueryClient(); const [isAddingDomain, setIsAddingDomain] = React.useState(false); const [editingDomain, setEditingDomain] = React.useState(null); @@ -275,7 +266,7 @@ export function AllowedDomainsSettings() {
{domain.domain}
- Default role: {getRoleName(domain.defaultRole)} + Default role: {getRoleLabel(domain.defaultRole)}
@@ -339,9 +330,9 @@ export function AllowedDomainsSettings() { label="Default Role" value={String(newRole)} onValueChange={(v) => v !== null && setNewRole(Number(v))} - items={Object.fromEntries(ROLES.map((r) => [String(r.value), r.label]))} + items={signupRoleItems} > - {ROLES.map((role) => ( + {signupRoles.map((role) => ( {role.label} @@ -403,9 +394,9 @@ export function AllowedDomainsSettings() { onValueChange={(v) => v !== null && editingDomain && handleUpdateRole(editingDomain.domain, Number(v)) } - items={Object.fromEntries(ROLES.map((r) => [String(r.value), r.label]))} + items={signupRoleItems} > - {ROLES.map((role) => ( + {signupRoles.map((role) => ( {role.label} diff --git a/packages/admin/src/components/settings/ApiTokenSettings.tsx b/packages/admin/src/components/settings/ApiTokenSettings.tsx index 86ba4fbd0..585996d71 100644 --- a/packages/admin/src/components/settings/ApiTokenSettings.tsx +++ b/packages/admin/src/components/settings/ApiTokenSettings.tsx @@ -5,6 +5,9 @@ */ import { Button, Checkbox, Input, Loader, Select } from "@cloudflare/kumo"; +import type { MessageDescriptor } from "@lingui/core"; +import { msg } from "@lingui/core/macro"; +import { useLingui } from "@lingui/react/macro"; import { ArrowLeft, Copy, @@ -25,6 +28,7 @@ import { revokeApiToken, API_TOKEN_SCOPES, type ApiTokenCreateResult, + type ApiTokenScopeValue, } from "../../lib/api/api-tokens.js"; import { getMutationError } from "../DialogError.js"; @@ -33,13 +37,59 @@ import { getMutationError } from "../DialogError.js"; // ============================================================================= const EXPIRY_OPTIONS = [ - { value: "none", label: "No expiry" }, - { value: "7d", label: "7 days" }, - { value: "30d", label: "30 days" }, - { value: "90d", label: "90 days" }, - { value: "365d", label: "1 year" }, + { value: "none", label: msg`No expiry` }, + { value: "7d", label: msg`7 days` }, + { value: "30d", label: msg`30 days` }, + { value: "90d", label: msg`90 days` }, + { value: "365d", label: msg`1 year` }, ] as const; +const API_TOKEN_SCOPE_VALUES: { + scope: ApiTokenScopeValue; + label: MessageDescriptor; + description: MessageDescriptor; +}[] = [ + { + scope: API_TOKEN_SCOPES.ContentRead, + label: msg`Content Read`, + description: msg`Read content entries`, + }, + { + scope: API_TOKEN_SCOPES.ContentWrite, + label: msg`Content Write`, + description: msg`Create, update, delete content`, + }, + { + scope: API_TOKEN_SCOPES.MediaRead, + label: msg`Media Read`, + description: msg`Read media files`, + }, + { + scope: API_TOKEN_SCOPES.MediaWrite, + label: msg`Media Write`, + description: msg`Upload and delete media`, + }, + { + scope: API_TOKEN_SCOPES.SchemaRead, + label: msg`Schema Read`, + description: msg`Read collection schemas`, + }, + { + scope: API_TOKEN_SCOPES.SchemaWrite, + label: msg`Schema Write`, + description: msg`Modify collection schemas`, + }, + { + scope: API_TOKEN_SCOPES.Admin, + label: msg`Admin`, + description: msg`Full admin access`, + }, +]; + +/** Wire scopes shown on the create-token form (contract-tested vs `API_TOKEN_SCOPES` and `@emdash-cms/auth`). */ +export const API_TOKEN_SCOPE_FORM_SCOPES: readonly ApiTokenScopeValue[] = + API_TOKEN_SCOPE_VALUES.map((row) => row.scope); + function computeExpiryDate(option: string): string | undefined { if (option === "none") return undefined; const days = parseInt(option, 10); @@ -54,6 +104,7 @@ function computeExpiryDate(option: string): string | undefined { // ============================================================================= export function ApiTokenSettings() { + const { t } = useLingui(); const queryClient = useQueryClient(); const [showCreateForm, setShowCreateForm] = React.useState(false); const [newToken, setNewToken] = React.useState(null); @@ -107,19 +158,24 @@ export function ApiTokenSettings() { } }; + const expirySelectItems = React.useMemo( + () => Object.fromEntries(EXPIRY_OPTIONS.map((o) => [o.value, t(o.label)])), + [t], + ); + return (
{/* Header */}
-
-

API Tokens

+

{t(msg`API Tokens`)}

- Create personal access tokens for programmatic API access + {t(msg`Create personal access tokens for programmatic API access`)}

@@ -131,10 +187,10 @@ export function ApiTokenSettings() {

- Token created: {newToken.info.name} + {t(msg`Token created: ${newToken.info.name}`)}

- Copy this token now — it won't be shown again. + {t(msg`Copy this token now — it won't be shown again.`)}

@@ -144,7 +200,7 @@ export function ApiTokenSettings() { variant="ghost" shape="square" onClick={() => setTokenVisible(!tokenVisible)} - aria-label={tokenVisible ? "Hide token" : "Show token"} + aria-label={tokenVisible ? t(msg`Hide token`) : t(msg`Show token`)} > {tokenVisible ? : } @@ -152,14 +208,14 @@ export function ApiTokenSettings() { variant="ghost" shape="square" onClick={handleCopyToken} - aria-label="Copy token" + aria-label={t(msg`Copy token`)} >
{copied && (

- Copied to clipboard + {t(msg`Copied to clipboard`)}

)}
@@ -167,9 +223,9 @@ export function ApiTokenSettings() { variant="ghost" size="sm" onClick={() => setNewToken(null)} - aria-label="Dismiss" + aria-label={t(msg`Dismiss`)} > - Dismiss + {t(msg`Dismiss`)}
@@ -178,6 +234,7 @@ export function ApiTokenSettings() { {/* Create form */} {showCreateForm ? ( @@ -191,7 +248,7 @@ export function ApiTokenSettings() { /> ) : ( )} @@ -203,7 +260,7 @@ export function ApiTokenSettings() { ) : !tokens || tokens.length === 0 ? (
- No API tokens yet. Create one to get started. + {t(msg`No API tokens yet. Create one to get started.`)}
) : (
@@ -217,16 +274,20 @@ export function ApiTokenSettings() {
- Scopes: {token.scopes.join(", ")} + {t(msg`Scopes: ${token.scopes.join(", ")}`)} {token.expiresAt && ( - Expires {new Date(token.expiresAt).toLocaleDateString()} + + {t(msg`Expires ${new Date(token.expiresAt).toLocaleDateString()}`)} + )} {token.lastUsedAt && ( - Last used {new Date(token.lastUsedAt).toLocaleDateString()} + + {t(msg`Last used ${new Date(token.lastUsedAt).toLocaleDateString()}`)} + )}
- Created {new Date(token.createdAt).toLocaleDateString()} + {t(msg`Created ${new Date(token.createdAt).toLocaleDateString()}`)}
@@ -237,14 +298,14 @@ export function ApiTokenSettings() { {getMutationError(revokeMutation.error)} )} - Revoke? + {t(msg`Revoke?`)} ) : ( @@ -262,7 +323,7 @@ export function ApiTokenSettings() { variant="ghost" shape="square" onClick={() => setRevokeConfirmId(token.id)} - aria-label="Revoke token" + aria-label={t(msg`Revoke token`)} > @@ -281,13 +342,21 @@ export function ApiTokenSettings() { // ============================================================================= interface CreateTokenFormProps { + expirySelectItems: Record; isCreating: boolean; error: string | null; onSubmit: (input: { name: string; scopes: string[]; expiresAt?: string }) => void; onCancel: () => void; } -function CreateTokenForm({ isCreating, error, onSubmit, onCancel }: CreateTokenFormProps) { +function CreateTokenForm({ + expirySelectItems, + isCreating, + error, + onSubmit, + onCancel, +}: CreateTokenFormProps) { + const { t } = useLingui(); const [name, setName] = React.useState(""); const [selectedScopes, setSelectedScopes] = React.useState>(new Set()); const [expiry, setExpiry] = React.useState("30d"); @@ -317,7 +386,7 @@ function CreateTokenForm({ isCreating, error, onSubmit, onCancel }: CreateTokenF return (
-

Create New Token

+

{t(msg`Create New Token`)}

{error && (
@@ -328,51 +397,53 @@ function CreateTokenForm({ isCreating, error, onSubmit, onCancel }: CreateTokenF
setName(e.target.value)} - placeholder="e.g., CI/CD Pipeline" + placeholder={t(msg`e.g., CI/CD Pipeline`)} required autoFocus />
-
Scopes
+
{t(msg`Scopes`)}
- {API_TOKEN_SCOPES.map((scope) => ( - - ))} + {API_TOKEN_SCOPE_VALUES.map(({ scope, label, description }) => { + return ( + + ); + })}
diff --git a/packages/admin/src/components/settings/useAllowedDomainsRolesConfig.ts b/packages/admin/src/components/settings/useAllowedDomainsRolesConfig.ts new file mode 100644 index 000000000..1d3fda242 --- /dev/null +++ b/packages/admin/src/components/settings/useAllowedDomainsRolesConfig.ts @@ -0,0 +1,34 @@ +import * as React from "react"; + +import type { RolesSelectRow } from "../users/useRolesConfig.js"; +import { useRolesConfig } from "../users/useRolesConfig.js"; + +/** Self-signup default role must not be Admin (API / product rule). */ +const MAX_SELF_SIGNUP_DEFAULT_ROLE = 40; + +/** + * Role labels and selects for Allowed Domains (Subscriber–Editor only for defaults). + * Built on {@link useRolesConfig}; keeps the filter + `Select` `items` shape in one place. + */ +export function useAllowedDomainsRolesConfig(): { + getRoleLabel: (level: number) => string; + signupRoles: readonly RolesSelectRow[]; + signupRoleItems: Record; +} { + const { roleLabels, getRoleLabel, roles } = useRolesConfig(); + + const signupRoles = React.useMemo( + () => roles.filter((r) => r.value <= MAX_SELF_SIGNUP_DEFAULT_ROLE), + [roles], + ); + + const signupRoleItems = React.useMemo(() => { + const entries: [string, string][] = signupRoles.map((r) => { + const label = roleLabels[String(r.value)]; + return [String(r.value), label ?? getRoleLabel(r.value)]; + }); + return Object.fromEntries(entries) as Record; + }, [signupRoles, roleLabels, getRoleLabel]); + + return { getRoleLabel, signupRoles, signupRoleItems }; +} diff --git a/packages/admin/src/components/users/InviteUserModal.tsx b/packages/admin/src/components/users/InviteUserModal.tsx index c704ee8a2..20ff5a822 100644 --- a/packages/admin/src/components/users/InviteUserModal.tsx +++ b/packages/admin/src/components/users/InviteUserModal.tsx @@ -2,7 +2,7 @@ import { Button, Dialog, Input, Select } from "@cloudflare/kumo"; import { Check, Copy, X } from "@phosphor-icons/react"; import * as React from "react"; -import { ROLES } from "./RoleBadge"; +import { useRolesConfig } from "./useRolesConfig.js"; export interface InviteUserModalProps { open: boolean; @@ -25,6 +25,7 @@ export function InviteUserModal({ onOpenChange, onInvite, }: InviteUserModalProps) { + const { roles, roleLabels } = useRolesConfig(); const [email, setEmail] = React.useState(""); const [role, setRole] = React.useState(30); // Default to Author const [copied, setCopied] = React.useState(false); @@ -163,9 +164,9 @@ export function InviteUserModal({ label="Role" value={role.toString()} onValueChange={(v) => v !== null && setRole(parseInt(v, 10))} - items={Object.fromEntries(ROLES.map((r) => [r.value.toString(), r.label]))} + items={roleLabels} > - {ROLES.map((r) => ( + {roles.map((r) => (
{r.label}
diff --git a/packages/admin/src/components/users/RoleBadge.tsx b/packages/admin/src/components/users/RoleBadge.tsx index 49be48a08..cab2776c3 100644 --- a/packages/admin/src/components/users/RoleBadge.tsx +++ b/packages/admin/src/components/users/RoleBadge.tsx @@ -1,49 +1,9 @@ -import { cn } from "../../lib/utils"; - -/** Role level to name mapping */ -const ROLE_CONFIG: Record = { - 10: { - label: "Subscriber", - color: "gray", - description: "Can view content", - }, - 20: { - label: "Contributor", - color: "blue", - description: "Can create content", - }, - 30: { - label: "Author", - color: "green", - description: "Can publish own content", - }, - 40: { - label: "Editor", - color: "purple", - description: "Can manage all content", - }, - 50: { - label: "Admin", - color: "red", - description: "Full access", - }, -}; +import { useLingui } from "@lingui/react/macro"; -/** Get role config, with fallback for unknown roles */ -export function getRoleConfig(role: number) { - return ( - ROLE_CONFIG[role] ?? { - label: `Role ${role}`, - color: "gray", - description: "Unknown role", - } - ); -} +import { cn } from "../../lib/utils"; +import { getRoleConfig } from "./roleDefinitions.js"; -/** Get role label from role level */ -export function getRoleLabel(role: number): string { - return getRoleConfig(role).label; -} +export type { RoleLevelConfig } from "./roleDefinitions.js"; export interface RoleBadgeProps { role: number; @@ -61,6 +21,7 @@ export function RoleBadge({ showDescription = false, className, }: RoleBadgeProps) { + const { t } = useLingui(); const config = getRoleConfig(role); const colorClasses: Record = { @@ -84,19 +45,10 @@ export function RoleBadge({ colorClasses[config.color], className, )} - title={showDescription ? undefined : config.description} + title={showDescription ? undefined : t(config.description)} > - {config.label} - {showDescription && - {config.description}} + {t(config.label)} + {showDescription && - {t(config.description)}} ); } - -/** List of all roles for dropdowns */ -export const ROLES = [ - { value: 10, label: "Subscriber", description: "Can view content" }, - { value: 20, label: "Contributor", description: "Can create content" }, - { value: 30, label: "Author", description: "Can publish own content" }, - { value: 40, label: "Editor", description: "Can manage all content" }, - { value: 50, label: "Admin", description: "Full access" }, -]; diff --git a/packages/admin/src/components/users/UserDetail.tsx b/packages/admin/src/components/users/UserDetail.tsx index 42f66a7fc..bedf814dd 100644 --- a/packages/admin/src/components/users/UserDetail.tsx +++ b/packages/admin/src/components/users/UserDetail.tsx @@ -13,7 +13,7 @@ import * as React from "react"; import type { UserDetail as UserDetailType, UpdateUserInput } from "../../lib/api"; import { useStableCallback } from "../../lib/hooks"; import { cn } from "../../lib/utils"; -import { ROLES, getRoleLabel } from "./RoleBadge"; +import { useRolesConfig } from "./useRolesConfig.js"; export interface UserDetailProps { user: UserDetailType | null; @@ -49,6 +49,7 @@ export function UserDetail({ onEnable, onSendRecovery, }: UserDetailProps) { + const { roles, roleLabels, getRoleLabel } = useRolesConfig(); const [name, setName] = React.useState(user?.name ?? ""); const [email, setEmail] = React.useState(user?.email ?? ""); const [role, setRole] = React.useState(user?.role ?? 30); @@ -184,9 +185,9 @@ export function UserDetail({ label="Role" value={role.toString()} onValueChange={(v) => v !== null && setRole(parseInt(v, 10))} - items={Object.fromEntries(ROLES.map((r) => [r.value.toString(), r.label]))} + items={roleLabels} > - {ROLES.map((r) => ( + {roles.map((r) => (
{r.label}
diff --git a/packages/admin/src/components/users/UserList.tsx b/packages/admin/src/components/users/UserList.tsx index f08a8f29d..bc0e54380 100644 --- a/packages/admin/src/components/users/UserList.tsx +++ b/packages/admin/src/components/users/UserList.tsx @@ -1,10 +1,12 @@ import { Button, Input, Loader, Select } from "@cloudflare/kumo"; +import { useLingui } from "@lingui/react/macro"; import { MagnifyingGlass, UserPlus, Prohibit, CheckCircle } from "@phosphor-icons/react"; import * as React from "react"; import type { UserListItem } from "../../lib/api"; import { cn } from "../../lib/utils"; -import { RoleBadge, ROLES } from "./RoleBadge"; +import { RoleBadge } from "./RoleBadge"; +import { useRolesConfig } from "./useRolesConfig.js"; export interface UserListProps { users: UserListItem[]; @@ -34,6 +36,17 @@ export function UserList({ onInviteUser, onLoadMore, }: UserListProps) { + const { t } = useLingui(); + const { roles, roleLabels } = useRolesConfig(); + const roleFilterSelectItems = React.useMemo( + () => ({ all: t`All roles`, ...roleLabels }), + [t, roleLabels], + ); + const roleFilterSelectOptions = React.useMemo( + () => [{ value: "all", label: t`All roles` }, ...roles], + [t, roles], + ); + return (
{/* Header */} @@ -65,16 +78,12 @@ export function UserList({ onValueChange={(value) => onRoleFilterChange(value === "all" || value === null ? undefined : parseInt(value, 10)) } - items={{ - all: "All roles", - ...Object.fromEntries(ROLES.map((r) => [r.value.toString(), r.label])), - }} + items={roleFilterSelectItems} aria-label="Filter by role" > - All roles - {ROLES.map((role) => ( - - {role.label} + {roleFilterSelectOptions.map((option) => ( + + {option.label} ))} diff --git a/packages/admin/src/components/users/index.ts b/packages/admin/src/components/users/index.ts index 01652c1ee..6933898ee 100644 --- a/packages/admin/src/components/users/index.ts +++ b/packages/admin/src/components/users/index.ts @@ -1,4 +1,6 @@ -export { RoleBadge, ROLES, getRoleConfig, getRoleLabel } from "./RoleBadge"; +export { RoleBadge } from "./RoleBadge"; +export { getRoleConfig } from "./roleDefinitions.js"; +export { useRolesConfig } from "./useRolesConfig.js"; export { UserList, UserListSkeleton } from "./UserList"; export { UserDetail } from "./UserDetail"; export { InviteUserModal } from "./InviteUserModal"; diff --git a/packages/admin/src/components/users/roleDefinitions.ts b/packages/admin/src/components/users/roleDefinitions.ts new file mode 100644 index 000000000..25dc23811 --- /dev/null +++ b/packages/admin/src/components/users/roleDefinitions.ts @@ -0,0 +1,70 @@ +import type { MessageDescriptor } from "@lingui/core"; +import { msg } from "@lingui/core/macro"; + +export type RoleLevelConfig = { + label: MessageDescriptor; + description: MessageDescriptor; + color: string; +}; + +/** + * Canonical role levels for admin UI (badge colors, selects, labels). + * Allowed Domains UI only offers default roles up to Editor (40), not Admin (50). + */ +export const ROLE_ENTRIES = [ + { + value: 10, + color: "gray", + label: msg`Subscriber`, + description: msg`Can view content`, + }, + { + value: 20, + color: "blue", + label: msg`Contributor`, + description: msg`Can create content`, + }, + { + value: 30, + color: "green", + label: msg`Author`, + description: msg`Can publish own content`, + }, + { + value: 40, + color: "purple", + label: msg`Editor`, + description: msg`Can manage all content`, + }, + { + value: 50, + color: "red", + label: msg`Admin`, + description: msg`Full access`, + }, +] as const satisfies readonly { + value: number; + color: string; + label: MessageDescriptor; + description: MessageDescriptor; +}[]; + +const ROLE_CONFIG: Record = Object.fromEntries( + ROLE_ENTRIES.map((e) => [ + e.value, + { label: e.label, description: e.description, color: e.color }, + ]), +); + +function unknownRoleConfig(role: number): RoleLevelConfig { + return { + label: msg`Role ${role}`, + description: msg`Unknown role`, + color: "gray", + }; +} + +/** Badge / display config for a numeric role level */ +export function getRoleConfig(role: number): RoleLevelConfig { + return ROLE_CONFIG[role] ?? unknownRoleConfig(role); +} diff --git a/packages/admin/src/components/users/useRolesConfig.ts b/packages/admin/src/components/users/useRolesConfig.ts new file mode 100644 index 000000000..3b3dc76f8 --- /dev/null +++ b/packages/admin/src/components/users/useRolesConfig.ts @@ -0,0 +1,46 @@ +import { msg } from "@lingui/core/macro"; +import { useLingui } from "@lingui/react/macro"; +import * as React from "react"; + +import { ROLE_ENTRIES } from "./roleDefinitions.js"; + +const MSG_ROLE_UNKNOWN = msg`Unknown`; + +export type RolesSelectRow = { + value: number; + label: string; + description: string; +}; + +/** + * Shared resolved role strings + descriptor rows for selects (after `i18n` is active). + */ +export function useRolesConfig(): { + roleLabels: Record; + getRoleLabel: (level: number) => string; + roles: readonly RolesSelectRow[]; +} { + const { t } = useLingui(); + + const roles = React.useMemo( + () => + ROLE_ENTRIES.map(({ value, label, description }) => ({ + value, + label: t(label), + description: t(description), + })), + [t], + ); + + const roleLabels = React.useMemo( + () => Object.fromEntries(ROLE_ENTRIES.map((r) => [String(r.value), t(r.label)])), + [t], + ); + + const getRoleLabel = React.useCallback( + (level: number) => roleLabels[String(level)] ?? t(MSG_ROLE_UNKNOWN), + [roleLabels, t], + ); + + return { roleLabels, getRoleLabel, roles }; +} diff --git a/packages/admin/src/lib/api/api-tokens.ts b/packages/admin/src/lib/api/api-tokens.ts index 8cc2b1cbc..1389e8bbb 100644 --- a/packages/admin/src/lib/api/api-tokens.ts +++ b/packages/admin/src/lib/api/api-tokens.ts @@ -35,16 +35,21 @@ export interface CreateApiTokenInput { expiresAt?: string; } -/** Available scopes for API tokens */ -export const API_TOKEN_SCOPES = [ - { value: "content:read", label: "Content Read", description: "Read content entries" }, - { value: "content:write", label: "Content Write", description: "Create, update, delete content" }, - { value: "media:read", label: "Media Read", description: "Read media files" }, - { value: "media:write", label: "Media Write", description: "Upload and delete media" }, - { value: "schema:read", label: "Schema Read", description: "Read collection schemas" }, - { value: "schema:write", label: "Schema Write", description: "Modify collection schemas" }, - { value: "admin", label: "Admin", description: "Full admin access" }, -] as const; +/** + * Scope strings for personal API tokens (wire + UI iteration order). + * Human-readable copy lives in `ApiTokenSettings` (`SCOPE_UI` + Lingui). + */ +export const API_TOKEN_SCOPES = { + ContentRead: "content:read", + ContentWrite: "content:write", + MediaRead: "media:read", + MediaWrite: "media:write", + SchemaRead: "schema:read", + SchemaWrite: "schema:write", + Admin: "admin", +} as const; + +export type ApiTokenScopeValue = (typeof API_TOKEN_SCOPES)[keyof typeof API_TOKEN_SCOPES]; // ============================================================================= // API Functions diff --git a/packages/admin/src/lib/api/index.ts b/packages/admin/src/lib/api/index.ts index 5511bc552..f5ce5bf23 100644 --- a/packages/admin/src/lib/api/index.ts +++ b/packages/admin/src/lib/api/index.ts @@ -257,6 +257,7 @@ export { type ApiTokenInfo, type ApiTokenCreateResult, type CreateApiTokenInput, + type ApiTokenScopeValue, API_TOKEN_SCOPES, fetchApiTokens, createApiToken, diff --git a/packages/admin/src/locales/de/messages.po b/packages/admin/src/locales/de/messages.po index f94b1bc24..26678dd8d 100644 --- a/packages/admin/src/locales/de/messages.po +++ b/packages/admin/src/locales/de/messages.po @@ -25,27 +25,101 @@ msgstr "{label} — keine Übersetzung" msgid "{label} — view translation" msgstr "{label} — Übersetzung anzeigen" +#: packages/admin/src/components/settings/ApiTokenSettings.tsx:44 +msgid "1 year" +msgstr "" + +#: packages/admin/src/components/settings/ApiTokenSettings.tsx:42 +msgid "30 days" +msgstr "" + +#: packages/admin/src/components/settings/ApiTokenSettings.tsx:41 +msgid "7 days" +msgstr "" + +#: packages/admin/src/components/settings/ApiTokenSettings.tsx:43 +msgid "90 days" +msgstr "" + +#: packages/admin/src/components/settings/ApiTokenSettings.tsx:84 +#: packages/admin/src/components/users/roleDefinitions.ts:42 +msgid "Admin" +msgstr "" + +#: packages/admin/src/components/WelcomeModal.tsx:25 +msgid "Administrator" +msgstr "" + #: packages/admin/src/components/LocaleSwitcher.tsx:68 msgid "All locales" msgstr "Alle Sprachen" +#: packages/admin/src/components/users/UserList.tsx:42 +#: packages/admin/src/components/users/UserList.tsx:46 +msgid "All roles" +msgstr "" + #: packages/admin/src/components/Settings.tsx:99 msgid "Allow users from specific domains to sign up" msgstr "Benutzern von bestimmten Domains die Registrierung erlauben" #: packages/admin/src/components/Settings.tsx:109 +#: packages/admin/src/components/settings/ApiTokenSettings.tsx:176 msgid "API Tokens" msgstr "API-Tokens" +#: packages/admin/src/components/WelcomeModal.tsx:53 +msgid "As an administrator, you can invite other users from the Users section." +msgstr "" + #: packages/admin/src/components/LoginPage.tsx:253 msgid "Authentication error: {error}" msgstr "Authentifizierungsfehler: {error}" +#: packages/admin/src/components/users/roleDefinitions.ts:30 +#: packages/admin/src/components/WelcomeModal.tsx:27 +msgid "Author" +msgstr "" + #: packages/admin/src/components/LoginPage.tsx:174 #: packages/admin/src/components/LoginPage.tsx:210 msgid "Back to login" msgstr "Zurück zur Anmeldung" +#: packages/admin/src/components/settings/ApiTokenSettings.tsx:171 +msgid "Back to settings" +msgstr "" + +#: packages/admin/src/components/editor/BlockMenu.tsx:101 +#: packages/admin/src/components/PortableTextEditor.tsx:729 +msgid "Bullet List" +msgstr "" + +#: packages/admin/src/components/users/roleDefinitions.ts:25 +msgid "Can create content" +msgstr "" + +#: packages/admin/src/components/users/roleDefinitions.ts:37 +msgid "Can manage all content" +msgstr "" + +#: packages/admin/src/components/users/roleDefinitions.ts:31 +msgid "Can publish own content" +msgstr "" + +#: packages/admin/src/components/users/roleDefinitions.ts:19 +msgid "Can view content" +msgstr "" + +#: packages/admin/src/components/settings/ApiTokenSettings.tsx:318 +#: packages/admin/src/components/settings/ApiTokenSettings.tsx:446 +msgid "Cancel" +msgstr "" + +#: packages/admin/src/components/AdminCommandPalette.tsx:193 +msgid "Categories" +msgstr "" + #: packages/admin/src/components/LoginPage.tsx:158 msgid "Check your email" msgstr "Überprüfen Sie Ihre E-Mail" @@ -58,14 +132,134 @@ msgstr "Wählen Sie Ihre bevorzugte Admin-Sprache" msgid "Click the link in the email to sign in." msgstr "Klicken Sie auf den Link in der E-Mail, um sich anzumelden." +#: packages/admin/src/components/WelcomeModal.tsx:54 +msgid "Close" +msgstr "" + +#: packages/admin/src/components/editor/BlockMenu.tsx:93 +#: packages/admin/src/components/PortableTextEditor.tsx:759 +msgid "Code Block" +msgstr "" + +#: packages/admin/src/components/settings/ApiTokenSettings.tsx:308 +msgid "Confirm" +msgstr "" + +#: packages/admin/src/components/AdminCommandPalette.tsx:365 +#: packages/admin/src/components/PortableTextEditor.tsx:1431 +msgid "Content" +msgstr "" + +#: packages/admin/src/components/Widgets.tsx:88 +msgid "Content Block" +msgstr "" + +#: packages/admin/src/components/settings/ApiTokenSettings.tsx:54 +msgid "Content Read" +msgstr "" + +#: packages/admin/src/components/AdminCommandPalette.tsx:185 +msgid "Content Types" +msgstr "" + +#: packages/admin/src/components/settings/ApiTokenSettings.tsx:59 +msgid "Content Write" +msgstr "" + +#: packages/admin/src/components/users/roleDefinitions.ts:24 +#: packages/admin/src/components/WelcomeModal.tsx:28 +msgid "Contributor" +msgstr "" + +#: packages/admin/src/components/settings/ApiTokenSettings.tsx:218 +msgid "Copied to clipboard" +msgstr "" + +#: packages/admin/src/components/settings/ApiTokenSettings.tsx:193 +msgid "Copy this token now — it won't be shown again." +msgstr "" + +#: packages/admin/src/components/settings/ApiTokenSettings.tsx:211 +msgid "Copy token" +msgstr "" + +#: packages/admin/src/components/PortableTextEditor.tsx:730 +msgid "Create a bullet list" +msgstr "" + +#: packages/admin/src/components/PortableTextEditor.tsx:740 +msgid "Create a numbered list" +msgstr "" + +#: packages/admin/src/components/settings/ApiTokenSettings.tsx:389 +msgid "Create New Token" +msgstr "" + #: packages/admin/src/components/Settings.tsx:110 +#: packages/admin/src/components/settings/ApiTokenSettings.tsx:178 msgid "Create personal access tokens for programmatic API access" msgstr "Persönliche Zugangstokens für programmatischen API-Zugriff erstellen" +#: packages/admin/src/components/settings/ApiTokenSettings.tsx:251 +#: packages/admin/src/components/settings/ApiTokenSettings.tsx:443 +msgid "Create Token" +msgstr "" + +#: packages/admin/src/components/settings/ApiTokenSettings.tsx:60 +msgid "Create, update, delete content" +msgstr "" + +#. placeholder {0}: new Date(token.createdAt).toLocaleDateString() +#: packages/admin/src/components/settings/ApiTokenSettings.tsx:290 +msgid "Created {0}" +msgstr "" + +#: packages/admin/src/components/ContentTypeEditor.tsx:121 +msgid "Created At" +msgstr "" + +#: packages/admin/src/components/settings/ApiTokenSettings.tsx:443 +msgid "Creating..." +msgstr "" + +#: packages/admin/src/components/AdminCommandPalette.tsx:131 +msgid "Dashboard" +msgstr "" + +#: packages/admin/src/components/settings/ApiTokenSettings.tsx:226 +#: packages/admin/src/components/settings/ApiTokenSettings.tsx:228 +msgid "Dismiss" +msgstr "" + +#: packages/admin/src/components/Widgets.tsx:95 +msgid "Display a navigation menu" +msgstr "" + +#: packages/admin/src/components/PortableTextEditor.tsx:769 +msgid "Divider" +msgstr "" + #: packages/admin/src/components/LoginPage.tsx:358 msgid "Don't have an account? <0>Sign up" msgstr "Noch kein Konto? <0>Registrieren" +#: packages/admin/src/components/ContentTypeEditor.tsx:117 +msgid "draft, published, or archived" +msgstr "" + +#: packages/admin/src/components/ContentTypeEditor.tsx:69 +msgid "Drafts" +msgstr "" + +#: packages/admin/src/components/settings/ApiTokenSettings.tsx:403 +msgid "e.g., CI/CD Pipeline" +msgstr "" + +#: packages/admin/src/components/users/roleDefinitions.ts:36 +#: packages/admin/src/components/WelcomeModal.tsx:26 +msgid "Editor" +msgstr "" + #: packages/admin/src/components/Settings.tsx:115 msgid "Email" msgstr "E-Mail" @@ -74,23 +268,121 @@ msgstr "E-Mail" msgid "Email address" msgstr "E-Mail-Adresse" +#. placeholder {0}: block.label +#: packages/admin/src/components/PortableTextEditor.tsx:1443 +msgid "Embed a {0}" +msgstr "" + +#: packages/admin/src/components/PortableTextEditor.tsx:1446 +msgid "Embeds" +msgstr "" + +#: packages/admin/src/components/ContentTypeEditor.tsx:85 +msgid "Enable full-text search on this collection" +msgstr "" + +#. placeholder {0}: new Date(token.expiresAt).toLocaleDateString() +#: packages/admin/src/components/settings/ApiTokenSettings.tsx:280 +msgid "Expires {0}" +msgstr "" + +#: packages/admin/src/components/settings/ApiTokenSettings.tsx:429 +msgid "Expiry" +msgstr "" + #: packages/admin/src/components/LoginPage.tsx:127 #: packages/admin/src/components/LoginPage.tsx:132 msgid "Failed to send magic link" msgstr "Magic Link konnte nicht gesendet werden" +#: packages/admin/src/components/users/roleDefinitions.ts:43 +msgid "Full access" +msgstr "" + +#: packages/admin/src/components/settings/ApiTokenSettings.tsx:85 +msgid "Full admin access" +msgstr "" + #: packages/admin/src/components/Settings.tsx:69 msgid "General" msgstr "Allgemein" +#: packages/admin/src/components/WelcomeModal.tsx:143 +msgid "Get Started" +msgstr "" + +#: packages/admin/src/components/editor/BlockMenu.tsx:61 +#: packages/admin/src/components/PortableTextEditor.tsx:699 +msgid "Heading 1" +msgstr "" + +#: packages/admin/src/components/editor/BlockMenu.tsx:69 +#: packages/admin/src/components/PortableTextEditor.tsx:709 +msgid "Heading 2" +msgstr "" + +#: packages/admin/src/components/editor/BlockMenu.tsx:77 +#: packages/admin/src/components/PortableTextEditor.tsx:719 +msgid "Heading 3" +msgstr "" + +#: packages/admin/src/components/settings/ApiTokenSettings.tsx:203 +msgid "Hide token" +msgstr "" + +#: packages/admin/src/components/ContentTypeEditor.tsx:103 +msgid "ID" +msgstr "" + #: packages/admin/src/components/LoginPage.tsx:160 msgid "If an account exists for <0>{email}, we've sent a sign-in link." msgstr "Falls ein Konto für <0>{email} existiert, haben wir einen Anmeldelink gesendet." +#: packages/admin/src/components/PortableTextEditor.tsx:1413 +msgid "Image" +msgstr "" + +#: packages/admin/src/components/AdminCommandPalette.tsx:227 +msgid "Import" +msgstr "" + +#: packages/admin/src/components/PortableTextEditor.tsx:750 +msgid "Insert a blockquote" +msgstr "" + +#: packages/admin/src/components/PortableTextEditor.tsx:760 +msgid "Insert a code block" +msgstr "" + +#: packages/admin/src/components/PortableTextEditor.tsx:770 +msgid "Insert a horizontal rule" +msgstr "" + +#: packages/admin/src/components/PortableTextEditor.tsx:1428 +msgid "Insert a reusable section" +msgstr "" + +#: packages/admin/src/components/PortableTextEditor.tsx:1414 +msgid "Insert an image" +msgstr "" + #: packages/admin/src/components/Settings.tsx:129 msgid "Language" msgstr "Sprache" +#: packages/admin/src/components/PortableTextEditor.tsx:700 +msgid "Large section heading" +msgstr "" + +#. placeholder {0}: new Date(token.lastUsedAt).toLocaleDateString() +#: packages/admin/src/components/settings/ApiTokenSettings.tsx:285 +msgid "Last used {0}" +msgstr "" + +#: packages/admin/src/components/WelcomeModal.tsx:143 +msgid "Loading..." +msgstr "" + #: packages/admin/src/components/LocaleSwitcher.tsx:60 msgid "Locale" msgstr "Sprache" @@ -99,18 +391,181 @@ msgstr "Sprache" msgid "Manage your passkeys and authentication" msgstr "Passkeys und Authentifizierung verwalten" +#: packages/admin/src/components/PortableTextEditor.tsx:1417 +msgid "Media" +msgstr "" + +#: packages/admin/src/components/AdminCommandPalette.tsx:154 +msgid "Media Library" +msgstr "" + +#: packages/admin/src/components/settings/ApiTokenSettings.tsx:64 +msgid "Media Read" +msgstr "" + +#: packages/admin/src/components/settings/ApiTokenSettings.tsx:69 +msgid "Media Write" +msgstr "" + +#: packages/admin/src/components/PortableTextEditor.tsx:710 +msgid "Medium section heading" +msgstr "" + +#: packages/admin/src/components/Widgets.tsx:94 +msgid "Menu" +msgstr "" + +#: packages/admin/src/components/AdminCommandPalette.tsx:161 +msgid "Menus" +msgstr "" + +#: packages/admin/src/components/settings/ApiTokenSettings.tsx:80 +msgid "Modify collection schemas" +msgstr "" + +#: packages/admin/src/components/AdminCommandPalette.tsx:335 +msgid "Navigation" +msgstr "" + +#: packages/admin/src/components/AdminCommandPalette.tsx:466 +msgid "new tab" +msgstr "" + +#: packages/admin/src/components/settings/ApiTokenSettings.tsx:263 +msgid "No API tokens yet. Create one to get started." +msgstr "" + +#: packages/admin/src/components/settings/ApiTokenSettings.tsx:40 +msgid "No expiry" +msgstr "" + +#: packages/admin/src/components/PortableTextEditor.tsx:940 +msgid "No results" +msgstr "" + +#: packages/admin/src/components/AdminCommandPalette.tsx:452 +msgid "No results found" +msgstr "" + +#: packages/admin/src/components/editor/BlockMenu.tsx:109 +#: packages/admin/src/components/PortableTextEditor.tsx:739 +msgid "Numbered List" +msgstr "" + #: packages/admin/src/components/LoginPage.tsx:313 msgid "Or continue with" msgstr "Oder fortfahren mit" +#: packages/admin/src/components/editor/BlockMenu.tsx:53 +msgid "Paragraph" +msgstr "" + +#: packages/admin/src/components/AdminCommandPalette.tsx:219 +msgid "Plugins" +msgstr "" + +#: packages/admin/src/components/ContentTypeEditor.tsx:79 +msgid "Preview" +msgstr "" + +#: packages/admin/src/components/ContentTypeEditor.tsx:80 +msgid "Preview content before publishing" +msgstr "" + +#: packages/admin/src/components/ContentTypeEditor.tsx:133 +msgid "Published At" +msgstr "" + +#: packages/admin/src/components/editor/BlockMenu.tsx:85 +#: packages/admin/src/components/PortableTextEditor.tsx:749 +msgid "Quote" +msgstr "" + +#: packages/admin/src/components/settings/ApiTokenSettings.tsx:75 +msgid "Read collection schemas" +msgstr "" + +#: packages/admin/src/components/settings/ApiTokenSettings.tsx:55 +msgid "Read content entries" +msgstr "" + +#: packages/admin/src/components/settings/ApiTokenSettings.tsx:65 +msgid "Read media files" +msgstr "" + +#: packages/admin/src/components/ContentTypeEditor.tsx:74 +msgid "Revisions" +msgstr "" + +#: packages/admin/src/components/settings/ApiTokenSettings.tsx:326 +msgid "Revoke token" +msgstr "" + +#: packages/admin/src/components/settings/ApiTokenSettings.tsx:301 +msgid "Revoke?" +msgstr "" + +#: packages/admin/src/components/settings/ApiTokenSettings.tsx:308 +msgid "Revoking..." +msgstr "" + +#: packages/admin/src/components/Widgets.tsx:89 +msgid "Rich text content" +msgstr "" + +#: packages/admin/src/components/users/roleDefinitions.ts:61 +msgid "Role {role}" +msgstr "" + +#: packages/admin/src/components/ContentTypeEditor.tsx:70 +msgid "Save content as draft before publishing" +msgstr "" + +#: packages/admin/src/components/settings/ApiTokenSettings.tsx:74 +msgid "Schema Read" +msgstr "" + +#: packages/admin/src/components/settings/ApiTokenSettings.tsx:79 +msgid "Schema Write" +msgstr "" + +#: packages/admin/src/components/settings/ApiTokenSettings.tsx:409 +msgid "Scopes" +msgstr "" + +#. placeholder {0}: token.scopes.join(", ") +#: packages/admin/src/components/settings/ApiTokenSettings.tsx:277 +msgid "Scopes: {0}" +msgstr "" + +#: packages/admin/src/components/ContentTypeEditor.tsx:84 +msgid "Search" +msgstr "" + #: packages/admin/src/components/Settings.tsx:82 msgid "Search engine optimization and verification" msgstr "Suchmaschinenoptimierung und Verifizierung" +#: packages/admin/src/components/AdminCommandPalette.tsx:425 +msgid "Search pages and content..." +msgstr "" + +#: packages/admin/src/components/PortableTextEditor.tsx:1427 +msgid "Section" +msgstr "" + +#: packages/admin/src/components/AdminCommandPalette.tsx:177 +msgid "Sections" +msgstr "" + #: packages/admin/src/components/Settings.tsx:92 msgid "Security" msgstr "Sicherheit" +#: packages/admin/src/components/AdminCommandPalette.tsx:243 +msgid "Security Settings" +msgstr "" + #: packages/admin/src/components/Settings.tsx:98 msgid "Self-Signup Domains" msgstr "Selbstregistrierungs-Domains" @@ -127,10 +582,15 @@ msgstr "Wird gesendet..." msgid "SEO" msgstr "SEO" +#: packages/admin/src/components/AdminCommandPalette.tsx:235 #: packages/admin/src/components/Settings.tsx:62 msgid "Settings" msgstr "Einstellungen" +#: packages/admin/src/components/settings/ApiTokenSettings.tsx:203 +msgid "Show token" +msgstr "" + #: packages/admin/src/components/LoginPage.tsx:283 msgid "Sign in to your site" msgstr "Bei Ihrer Website anmelden" @@ -151,6 +611,14 @@ msgstr "Mit Passkey anmelden" msgid "Site identity, logo, favicon, and reading preferences" msgstr "Website-Identität, Logo, Favicon und Leseeinstellungen" +#: packages/admin/src/components/ContentTypeEditor.tsx:109 +msgid "Slug" +msgstr "" + +#: packages/admin/src/components/PortableTextEditor.tsx:720 +msgid "Small section heading" +msgstr "" + #: packages/admin/src/components/Settings.tsx:75 msgid "Social Links" msgstr "Soziale Netzwerke" @@ -159,14 +627,76 @@ msgstr "Soziale Netzwerke" msgid "Social media profile links" msgstr "Links zu Social-Media-Profilen" +#: packages/admin/src/components/ContentTypeEditor.tsx:115 +msgid "Status" +msgstr "" + +#: packages/admin/src/components/users/roleDefinitions.ts:18 +#: packages/admin/src/components/WelcomeModal.tsx:29 +msgid "Subscriber" +msgstr "" + +#: packages/admin/src/components/AdminCommandPalette.tsx:202 +msgid "Tags" +msgstr "" + #: packages/admin/src/components/LoginPage.tsx:170 msgid "The link will expire in 15 minutes." msgstr "Der Link ist 15 Minuten gültig." +#: packages/admin/src/components/AdminCommandPalette.tsx:470 +msgid "to close" +msgstr "" + +#: packages/admin/src/components/AdminCommandPalette.tsx:460 +msgid "to select" +msgstr "" + +#. placeholder {0}: newToken.info.name +#: packages/admin/src/components/settings/ApiTokenSettings.tsx:190 +msgid "Token created: {0}" +msgstr "" + +#: packages/admin/src/components/settings/ApiTokenSettings.tsx:400 +msgid "Token Name" +msgstr "" + +#: packages/admin/src/components/ContentTypeEditor.tsx:75 +msgid "Track content history" +msgstr "" + +#: packages/admin/src/components/ContentTypeEditor.tsx:105 +msgid "Unique identifier (ULID)" +msgstr "" + +#: packages/admin/src/components/users/useRolesConfig.ts:7 +msgid "Unknown" +msgstr "" + +#: packages/admin/src/components/users/roleDefinitions.ts:62 +msgid "Unknown role" +msgstr "" + +#: packages/admin/src/components/ContentTypeEditor.tsx:127 +msgid "Updated At" +msgstr "" + +#: packages/admin/src/components/settings/ApiTokenSettings.tsx:70 +msgid "Upload and delete media" +msgstr "" + +#: packages/admin/src/components/ContentTypeEditor.tsx:111 +msgid "URL-friendly identifier" +msgstr "" + #: packages/admin/src/components/LoginPage.tsx:351 msgid "Use your registered passkey to sign in securely." msgstr "Verwenden Sie Ihren registrierten Passkey, um sich sicher anzumelden." +#: packages/admin/src/components/AdminCommandPalette.tsx:211 +msgid "Users" +msgstr "" + #: packages/admin/src/components/Settings.tsx:116 msgid "View email provider status and send test emails" msgstr "E-Mail-Anbieter-Status anzeigen und Test-E-Mails senden" @@ -174,3 +704,51 @@ msgstr "E-Mail-Anbieter-Status anzeigen und Test-E-Mails senden" #: packages/admin/src/components/LoginPage.tsx:352 msgid "We'll send you a link to sign in without a password." msgstr "Wir senden Ihnen einen Link, um sich ohne Passwort anzumelden." + +#: packages/admin/src/components/WelcomeModal.tsx:96 +msgid "Welcome to EmDash, {firstName}!" +msgstr "" + +#: packages/admin/src/components/WelcomeModal.tsx:96 +msgid "Welcome to EmDash!" +msgstr "" + +#: packages/admin/src/components/ContentTypeEditor.tsx:123 +msgid "When the entry was created" +msgstr "" + +#: packages/admin/src/components/ContentTypeEditor.tsx:129 +msgid "When the entry was last modified" +msgstr "" + +#: packages/admin/src/components/ContentTypeEditor.tsx:135 +msgid "When the entry was published" +msgstr "" + +#: packages/admin/src/components/AdminCommandPalette.tsx:169 +msgid "Widgets" +msgstr "" + +#: packages/admin/src/components/WelcomeModal.tsx:43 +msgid "You can create and edit your own content." +msgstr "" + +#: packages/admin/src/components/WelcomeModal.tsx:42 +msgid "You can manage content, media, menus, and taxonomies." +msgstr "" + +#: packages/admin/src/components/WelcomeModal.tsx:44 +msgid "You can view and contribute to the site." +msgstr "" + +#: packages/admin/src/components/WelcomeModal.tsx:41 +msgid "You have full access to manage this site, including users, settings, and all content." +msgstr "" + +#: packages/admin/src/components/WelcomeModal.tsx:39 +msgid "Your account has been created successfully." +msgstr "" + +#: packages/admin/src/components/WelcomeModal.tsx:40 +msgid "Your Role" +msgstr "" diff --git a/packages/admin/src/locales/en/messages.po b/packages/admin/src/locales/en/messages.po index fd4296104..a028d41c0 100644 --- a/packages/admin/src/locales/en/messages.po +++ b/packages/admin/src/locales/en/messages.po @@ -25,27 +25,101 @@ msgstr "{label} — no translation" msgid "{label} — view translation" msgstr "{label} — view translation" +#: packages/admin/src/components/settings/ApiTokenSettings.tsx:44 +msgid "1 year" +msgstr "1 year" + +#: packages/admin/src/components/settings/ApiTokenSettings.tsx:42 +msgid "30 days" +msgstr "30 days" + +#: packages/admin/src/components/settings/ApiTokenSettings.tsx:41 +msgid "7 days" +msgstr "7 days" + +#: packages/admin/src/components/settings/ApiTokenSettings.tsx:43 +msgid "90 days" +msgstr "90 days" + +#: packages/admin/src/components/settings/ApiTokenSettings.tsx:84 +#: packages/admin/src/components/users/roleDefinitions.ts:42 +msgid "Admin" +msgstr "Admin" + +#: packages/admin/src/components/WelcomeModal.tsx:25 +msgid "Administrator" +msgstr "Administrator" + #: packages/admin/src/components/LocaleSwitcher.tsx:68 msgid "All locales" msgstr "All locales" +#: packages/admin/src/components/users/UserList.tsx:42 +#: packages/admin/src/components/users/UserList.tsx:46 +msgid "All roles" +msgstr "All roles" + #: packages/admin/src/components/Settings.tsx:99 msgid "Allow users from specific domains to sign up" msgstr "Allow users from specific domains to sign up" #: packages/admin/src/components/Settings.tsx:109 +#: packages/admin/src/components/settings/ApiTokenSettings.tsx:176 msgid "API Tokens" msgstr "API Tokens" +#: packages/admin/src/components/WelcomeModal.tsx:53 +msgid "As an administrator, you can invite other users from the Users section." +msgstr "As an administrator, you can invite other users from the Users section." + #: packages/admin/src/components/LoginPage.tsx:253 msgid "Authentication error: {error}" msgstr "Authentication error: {error}" +#: packages/admin/src/components/users/roleDefinitions.ts:30 +#: packages/admin/src/components/WelcomeModal.tsx:27 +msgid "Author" +msgstr "Author" + #: packages/admin/src/components/LoginPage.tsx:174 #: packages/admin/src/components/LoginPage.tsx:210 msgid "Back to login" msgstr "Back to login" +#: packages/admin/src/components/settings/ApiTokenSettings.tsx:171 +msgid "Back to settings" +msgstr "Back to settings" + +#: packages/admin/src/components/editor/BlockMenu.tsx:101 +#: packages/admin/src/components/PortableTextEditor.tsx:729 +msgid "Bullet List" +msgstr "Bullet List" + +#: packages/admin/src/components/users/roleDefinitions.ts:25 +msgid "Can create content" +msgstr "Can create content" + +#: packages/admin/src/components/users/roleDefinitions.ts:37 +msgid "Can manage all content" +msgstr "Can manage all content" + +#: packages/admin/src/components/users/roleDefinitions.ts:31 +msgid "Can publish own content" +msgstr "Can publish own content" + +#: packages/admin/src/components/users/roleDefinitions.ts:19 +msgid "Can view content" +msgstr "Can view content" + +#: packages/admin/src/components/settings/ApiTokenSettings.tsx:318 +#: packages/admin/src/components/settings/ApiTokenSettings.tsx:446 +msgid "Cancel" +msgstr "Cancel" + +#: packages/admin/src/components/AdminCommandPalette.tsx:193 +msgid "Categories" +msgstr "Categories" + #: packages/admin/src/components/LoginPage.tsx:158 msgid "Check your email" msgstr "Check your email" @@ -58,14 +132,134 @@ msgstr "Choose your preferred admin language" msgid "Click the link in the email to sign in." msgstr "Click the link in the email to sign in." +#: packages/admin/src/components/WelcomeModal.tsx:54 +msgid "Close" +msgstr "Close" + +#: packages/admin/src/components/editor/BlockMenu.tsx:93 +#: packages/admin/src/components/PortableTextEditor.tsx:759 +msgid "Code Block" +msgstr "Code Block" + +#: packages/admin/src/components/settings/ApiTokenSettings.tsx:308 +msgid "Confirm" +msgstr "Confirm" + +#: packages/admin/src/components/AdminCommandPalette.tsx:365 +#: packages/admin/src/components/PortableTextEditor.tsx:1431 +msgid "Content" +msgstr "Content" + +#: packages/admin/src/components/Widgets.tsx:88 +msgid "Content Block" +msgstr "Content Block" + +#: packages/admin/src/components/settings/ApiTokenSettings.tsx:54 +msgid "Content Read" +msgstr "Content Read" + +#: packages/admin/src/components/AdminCommandPalette.tsx:185 +msgid "Content Types" +msgstr "Content Types" + +#: packages/admin/src/components/settings/ApiTokenSettings.tsx:59 +msgid "Content Write" +msgstr "Content Write" + +#: packages/admin/src/components/users/roleDefinitions.ts:24 +#: packages/admin/src/components/WelcomeModal.tsx:28 +msgid "Contributor" +msgstr "Contributor" + +#: packages/admin/src/components/settings/ApiTokenSettings.tsx:218 +msgid "Copied to clipboard" +msgstr "Copied to clipboard" + +#: packages/admin/src/components/settings/ApiTokenSettings.tsx:193 +msgid "Copy this token now — it won't be shown again." +msgstr "Copy this token now — it won't be shown again." + +#: packages/admin/src/components/settings/ApiTokenSettings.tsx:211 +msgid "Copy token" +msgstr "Copy token" + +#: packages/admin/src/components/PortableTextEditor.tsx:730 +msgid "Create a bullet list" +msgstr "Create a bullet list" + +#: packages/admin/src/components/PortableTextEditor.tsx:740 +msgid "Create a numbered list" +msgstr "Create a numbered list" + +#: packages/admin/src/components/settings/ApiTokenSettings.tsx:389 +msgid "Create New Token" +msgstr "Create New Token" + #: packages/admin/src/components/Settings.tsx:110 +#: packages/admin/src/components/settings/ApiTokenSettings.tsx:178 msgid "Create personal access tokens for programmatic API access" msgstr "Create personal access tokens for programmatic API access" +#: packages/admin/src/components/settings/ApiTokenSettings.tsx:251 +#: packages/admin/src/components/settings/ApiTokenSettings.tsx:443 +msgid "Create Token" +msgstr "Create Token" + +#: packages/admin/src/components/settings/ApiTokenSettings.tsx:60 +msgid "Create, update, delete content" +msgstr "Create, update, delete content" + +#. placeholder {0}: new Date(token.createdAt).toLocaleDateString() +#: packages/admin/src/components/settings/ApiTokenSettings.tsx:290 +msgid "Created {0}" +msgstr "Created {0}" + +#: packages/admin/src/components/ContentTypeEditor.tsx:121 +msgid "Created At" +msgstr "Created At" + +#: packages/admin/src/components/settings/ApiTokenSettings.tsx:443 +msgid "Creating..." +msgstr "Creating..." + +#: packages/admin/src/components/AdminCommandPalette.tsx:131 +msgid "Dashboard" +msgstr "Dashboard" + +#: packages/admin/src/components/settings/ApiTokenSettings.tsx:226 +#: packages/admin/src/components/settings/ApiTokenSettings.tsx:228 +msgid "Dismiss" +msgstr "Dismiss" + +#: packages/admin/src/components/Widgets.tsx:95 +msgid "Display a navigation menu" +msgstr "Display a navigation menu" + +#: packages/admin/src/components/PortableTextEditor.tsx:769 +msgid "Divider" +msgstr "Divider" + #: packages/admin/src/components/LoginPage.tsx:358 msgid "Don't have an account? <0>Sign up" msgstr "Don't have an account? <0>Sign up" +#: packages/admin/src/components/ContentTypeEditor.tsx:117 +msgid "draft, published, or archived" +msgstr "draft, published, or archived" + +#: packages/admin/src/components/ContentTypeEditor.tsx:69 +msgid "Drafts" +msgstr "Drafts" + +#: packages/admin/src/components/settings/ApiTokenSettings.tsx:403 +msgid "e.g., CI/CD Pipeline" +msgstr "e.g., CI/CD Pipeline" + +#: packages/admin/src/components/users/roleDefinitions.ts:36 +#: packages/admin/src/components/WelcomeModal.tsx:26 +msgid "Editor" +msgstr "Editor" + #: packages/admin/src/components/Settings.tsx:115 msgid "Email" msgstr "Email" @@ -74,23 +268,121 @@ msgstr "Email" msgid "Email address" msgstr "Email address" +#. placeholder {0}: block.label +#: packages/admin/src/components/PortableTextEditor.tsx:1443 +msgid "Embed a {0}" +msgstr "Embed a {0}" + +#: packages/admin/src/components/PortableTextEditor.tsx:1446 +msgid "Embeds" +msgstr "Embeds" + +#: packages/admin/src/components/ContentTypeEditor.tsx:85 +msgid "Enable full-text search on this collection" +msgstr "Enable full-text search on this collection" + +#. placeholder {0}: new Date(token.expiresAt).toLocaleDateString() +#: packages/admin/src/components/settings/ApiTokenSettings.tsx:280 +msgid "Expires {0}" +msgstr "Expires {0}" + +#: packages/admin/src/components/settings/ApiTokenSettings.tsx:429 +msgid "Expiry" +msgstr "Expiry" + #: packages/admin/src/components/LoginPage.tsx:127 #: packages/admin/src/components/LoginPage.tsx:132 msgid "Failed to send magic link" msgstr "Failed to send magic link" +#: packages/admin/src/components/users/roleDefinitions.ts:43 +msgid "Full access" +msgstr "Full access" + +#: packages/admin/src/components/settings/ApiTokenSettings.tsx:85 +msgid "Full admin access" +msgstr "Full admin access" + #: packages/admin/src/components/Settings.tsx:69 msgid "General" msgstr "General" +#: packages/admin/src/components/WelcomeModal.tsx:143 +msgid "Get Started" +msgstr "Get Started" + +#: packages/admin/src/components/editor/BlockMenu.tsx:61 +#: packages/admin/src/components/PortableTextEditor.tsx:699 +msgid "Heading 1" +msgstr "Heading 1" + +#: packages/admin/src/components/editor/BlockMenu.tsx:69 +#: packages/admin/src/components/PortableTextEditor.tsx:709 +msgid "Heading 2" +msgstr "Heading 2" + +#: packages/admin/src/components/editor/BlockMenu.tsx:77 +#: packages/admin/src/components/PortableTextEditor.tsx:719 +msgid "Heading 3" +msgstr "Heading 3" + +#: packages/admin/src/components/settings/ApiTokenSettings.tsx:203 +msgid "Hide token" +msgstr "Hide token" + +#: packages/admin/src/components/ContentTypeEditor.tsx:103 +msgid "ID" +msgstr "ID" + #: packages/admin/src/components/LoginPage.tsx:160 msgid "If an account exists for <0>{email}, we've sent a sign-in link." msgstr "If an account exists for <0>{email}, we've sent a sign-in link." +#: packages/admin/src/components/PortableTextEditor.tsx:1413 +msgid "Image" +msgstr "Image" + +#: packages/admin/src/components/AdminCommandPalette.tsx:227 +msgid "Import" +msgstr "Import" + +#: packages/admin/src/components/PortableTextEditor.tsx:750 +msgid "Insert a blockquote" +msgstr "Insert a blockquote" + +#: packages/admin/src/components/PortableTextEditor.tsx:760 +msgid "Insert a code block" +msgstr "Insert a code block" + +#: packages/admin/src/components/PortableTextEditor.tsx:770 +msgid "Insert a horizontal rule" +msgstr "Insert a horizontal rule" + +#: packages/admin/src/components/PortableTextEditor.tsx:1428 +msgid "Insert a reusable section" +msgstr "Insert a reusable section" + +#: packages/admin/src/components/PortableTextEditor.tsx:1414 +msgid "Insert an image" +msgstr "Insert an image" + #: packages/admin/src/components/Settings.tsx:129 msgid "Language" msgstr "Language" +#: packages/admin/src/components/PortableTextEditor.tsx:700 +msgid "Large section heading" +msgstr "Large section heading" + +#. placeholder {0}: new Date(token.lastUsedAt).toLocaleDateString() +#: packages/admin/src/components/settings/ApiTokenSettings.tsx:285 +msgid "Last used {0}" +msgstr "Last used {0}" + +#: packages/admin/src/components/WelcomeModal.tsx:143 +msgid "Loading..." +msgstr "Loading..." + #: packages/admin/src/components/LocaleSwitcher.tsx:60 msgid "Locale" msgstr "Locale" @@ -99,18 +391,181 @@ msgstr "Locale" msgid "Manage your passkeys and authentication" msgstr "Manage your passkeys and authentication" +#: packages/admin/src/components/PortableTextEditor.tsx:1417 +msgid "Media" +msgstr "Media" + +#: packages/admin/src/components/AdminCommandPalette.tsx:154 +msgid "Media Library" +msgstr "Media Library" + +#: packages/admin/src/components/settings/ApiTokenSettings.tsx:64 +msgid "Media Read" +msgstr "Media Read" + +#: packages/admin/src/components/settings/ApiTokenSettings.tsx:69 +msgid "Media Write" +msgstr "Media Write" + +#: packages/admin/src/components/PortableTextEditor.tsx:710 +msgid "Medium section heading" +msgstr "Medium section heading" + +#: packages/admin/src/components/Widgets.tsx:94 +msgid "Menu" +msgstr "Menu" + +#: packages/admin/src/components/AdminCommandPalette.tsx:161 +msgid "Menus" +msgstr "Menus" + +#: packages/admin/src/components/settings/ApiTokenSettings.tsx:80 +msgid "Modify collection schemas" +msgstr "Modify collection schemas" + +#: packages/admin/src/components/AdminCommandPalette.tsx:335 +msgid "Navigation" +msgstr "Navigation" + +#: packages/admin/src/components/AdminCommandPalette.tsx:466 +msgid "new tab" +msgstr "new tab" + +#: packages/admin/src/components/settings/ApiTokenSettings.tsx:263 +msgid "No API tokens yet. Create one to get started." +msgstr "No API tokens yet. Create one to get started." + +#: packages/admin/src/components/settings/ApiTokenSettings.tsx:40 +msgid "No expiry" +msgstr "No expiry" + +#: packages/admin/src/components/PortableTextEditor.tsx:940 +msgid "No results" +msgstr "No results" + +#: packages/admin/src/components/AdminCommandPalette.tsx:452 +msgid "No results found" +msgstr "No results found" + +#: packages/admin/src/components/editor/BlockMenu.tsx:109 +#: packages/admin/src/components/PortableTextEditor.tsx:739 +msgid "Numbered List" +msgstr "Numbered List" + #: packages/admin/src/components/LoginPage.tsx:313 msgid "Or continue with" msgstr "Or continue with" +#: packages/admin/src/components/editor/BlockMenu.tsx:53 +msgid "Paragraph" +msgstr "Paragraph" + +#: packages/admin/src/components/AdminCommandPalette.tsx:219 +msgid "Plugins" +msgstr "Plugins" + +#: packages/admin/src/components/ContentTypeEditor.tsx:79 +msgid "Preview" +msgstr "Preview" + +#: packages/admin/src/components/ContentTypeEditor.tsx:80 +msgid "Preview content before publishing" +msgstr "Preview content before publishing" + +#: packages/admin/src/components/ContentTypeEditor.tsx:133 +msgid "Published At" +msgstr "Published At" + +#: packages/admin/src/components/editor/BlockMenu.tsx:85 +#: packages/admin/src/components/PortableTextEditor.tsx:749 +msgid "Quote" +msgstr "Quote" + +#: packages/admin/src/components/settings/ApiTokenSettings.tsx:75 +msgid "Read collection schemas" +msgstr "Read collection schemas" + +#: packages/admin/src/components/settings/ApiTokenSettings.tsx:55 +msgid "Read content entries" +msgstr "Read content entries" + +#: packages/admin/src/components/settings/ApiTokenSettings.tsx:65 +msgid "Read media files" +msgstr "Read media files" + +#: packages/admin/src/components/ContentTypeEditor.tsx:74 +msgid "Revisions" +msgstr "Revisions" + +#: packages/admin/src/components/settings/ApiTokenSettings.tsx:326 +msgid "Revoke token" +msgstr "Revoke token" + +#: packages/admin/src/components/settings/ApiTokenSettings.tsx:301 +msgid "Revoke?" +msgstr "Revoke?" + +#: packages/admin/src/components/settings/ApiTokenSettings.tsx:308 +msgid "Revoking..." +msgstr "Revoking..." + +#: packages/admin/src/components/Widgets.tsx:89 +msgid "Rich text content" +msgstr "Rich text content" + +#: packages/admin/src/components/users/roleDefinitions.ts:61 +msgid "Role {role}" +msgstr "Role {role}" + +#: packages/admin/src/components/ContentTypeEditor.tsx:70 +msgid "Save content as draft before publishing" +msgstr "Save content as draft before publishing" + +#: packages/admin/src/components/settings/ApiTokenSettings.tsx:74 +msgid "Schema Read" +msgstr "Schema Read" + +#: packages/admin/src/components/settings/ApiTokenSettings.tsx:79 +msgid "Schema Write" +msgstr "Schema Write" + +#: packages/admin/src/components/settings/ApiTokenSettings.tsx:409 +msgid "Scopes" +msgstr "Scopes" + +#. placeholder {0}: token.scopes.join(", ") +#: packages/admin/src/components/settings/ApiTokenSettings.tsx:277 +msgid "Scopes: {0}" +msgstr "Scopes: {0}" + +#: packages/admin/src/components/ContentTypeEditor.tsx:84 +msgid "Search" +msgstr "Search" + #: packages/admin/src/components/Settings.tsx:82 msgid "Search engine optimization and verification" msgstr "Search engine optimization and verification" +#: packages/admin/src/components/AdminCommandPalette.tsx:425 +msgid "Search pages and content..." +msgstr "Search pages and content..." + +#: packages/admin/src/components/PortableTextEditor.tsx:1427 +msgid "Section" +msgstr "Section" + +#: packages/admin/src/components/AdminCommandPalette.tsx:177 +msgid "Sections" +msgstr "Sections" + #: packages/admin/src/components/Settings.tsx:92 msgid "Security" msgstr "Security" +#: packages/admin/src/components/AdminCommandPalette.tsx:243 +msgid "Security Settings" +msgstr "Security Settings" + #: packages/admin/src/components/Settings.tsx:98 msgid "Self-Signup Domains" msgstr "Self-Signup Domains" @@ -127,10 +582,15 @@ msgstr "Sending..." msgid "SEO" msgstr "SEO" +#: packages/admin/src/components/AdminCommandPalette.tsx:235 #: packages/admin/src/components/Settings.tsx:62 msgid "Settings" msgstr "Settings" +#: packages/admin/src/components/settings/ApiTokenSettings.tsx:203 +msgid "Show token" +msgstr "Show token" + #: packages/admin/src/components/LoginPage.tsx:283 msgid "Sign in to your site" msgstr "Sign in to your site" @@ -151,6 +611,14 @@ msgstr "Sign in with Passkey" msgid "Site identity, logo, favicon, and reading preferences" msgstr "Site identity, logo, favicon, and reading preferences" +#: packages/admin/src/components/ContentTypeEditor.tsx:109 +msgid "Slug" +msgstr "Slug" + +#: packages/admin/src/components/PortableTextEditor.tsx:720 +msgid "Small section heading" +msgstr "Small section heading" + #: packages/admin/src/components/Settings.tsx:75 msgid "Social Links" msgstr "Social Links" @@ -159,14 +627,76 @@ msgstr "Social Links" msgid "Social media profile links" msgstr "Social media profile links" +#: packages/admin/src/components/ContentTypeEditor.tsx:115 +msgid "Status" +msgstr "Status" + +#: packages/admin/src/components/users/roleDefinitions.ts:18 +#: packages/admin/src/components/WelcomeModal.tsx:29 +msgid "Subscriber" +msgstr "Subscriber" + +#: packages/admin/src/components/AdminCommandPalette.tsx:202 +msgid "Tags" +msgstr "Tags" + #: packages/admin/src/components/LoginPage.tsx:170 msgid "The link will expire in 15 minutes." msgstr "The link will expire in 15 minutes." +#: packages/admin/src/components/AdminCommandPalette.tsx:470 +msgid "to close" +msgstr "to close" + +#: packages/admin/src/components/AdminCommandPalette.tsx:460 +msgid "to select" +msgstr "to select" + +#. placeholder {0}: newToken.info.name +#: packages/admin/src/components/settings/ApiTokenSettings.tsx:190 +msgid "Token created: {0}" +msgstr "Token created: {0}" + +#: packages/admin/src/components/settings/ApiTokenSettings.tsx:400 +msgid "Token Name" +msgstr "Token Name" + +#: packages/admin/src/components/ContentTypeEditor.tsx:75 +msgid "Track content history" +msgstr "Track content history" + +#: packages/admin/src/components/ContentTypeEditor.tsx:105 +msgid "Unique identifier (ULID)" +msgstr "Unique identifier (ULID)" + +#: packages/admin/src/components/users/useRolesConfig.ts:7 +msgid "Unknown" +msgstr "Unknown" + +#: packages/admin/src/components/users/roleDefinitions.ts:62 +msgid "Unknown role" +msgstr "Unknown role" + +#: packages/admin/src/components/ContentTypeEditor.tsx:127 +msgid "Updated At" +msgstr "Updated At" + +#: packages/admin/src/components/settings/ApiTokenSettings.tsx:70 +msgid "Upload and delete media" +msgstr "Upload and delete media" + +#: packages/admin/src/components/ContentTypeEditor.tsx:111 +msgid "URL-friendly identifier" +msgstr "URL-friendly identifier" + #: packages/admin/src/components/LoginPage.tsx:351 msgid "Use your registered passkey to sign in securely." msgstr "Use your registered passkey to sign in securely." +#: packages/admin/src/components/AdminCommandPalette.tsx:211 +msgid "Users" +msgstr "Users" + #: packages/admin/src/components/Settings.tsx:116 msgid "View email provider status and send test emails" msgstr "View email provider status and send test emails" @@ -174,3 +704,51 @@ msgstr "View email provider status and send test emails" #: packages/admin/src/components/LoginPage.tsx:352 msgid "We'll send you a link to sign in without a password." msgstr "We'll send you a link to sign in without a password." + +#: packages/admin/src/components/WelcomeModal.tsx:96 +msgid "Welcome to EmDash, {firstName}!" +msgstr "Welcome to EmDash, {firstName}!" + +#: packages/admin/src/components/WelcomeModal.tsx:96 +msgid "Welcome to EmDash!" +msgstr "Welcome to EmDash!" + +#: packages/admin/src/components/ContentTypeEditor.tsx:123 +msgid "When the entry was created" +msgstr "When the entry was created" + +#: packages/admin/src/components/ContentTypeEditor.tsx:129 +msgid "When the entry was last modified" +msgstr "When the entry was last modified" + +#: packages/admin/src/components/ContentTypeEditor.tsx:135 +msgid "When the entry was published" +msgstr "When the entry was published" + +#: packages/admin/src/components/AdminCommandPalette.tsx:169 +msgid "Widgets" +msgstr "Widgets" + +#: packages/admin/src/components/WelcomeModal.tsx:43 +msgid "You can create and edit your own content." +msgstr "You can create and edit your own content." + +#: packages/admin/src/components/WelcomeModal.tsx:42 +msgid "You can manage content, media, menus, and taxonomies." +msgstr "You can manage content, media, menus, and taxonomies." + +#: packages/admin/src/components/WelcomeModal.tsx:44 +msgid "You can view and contribute to the site." +msgstr "You can view and contribute to the site." + +#: packages/admin/src/components/WelcomeModal.tsx:41 +msgid "You have full access to manage this site, including users, settings, and all content." +msgstr "You have full access to manage this site, including users, settings, and all content." + +#: packages/admin/src/components/WelcomeModal.tsx:39 +msgid "Your account has been created successfully." +msgstr "Your account has been created successfully." + +#: packages/admin/src/components/WelcomeModal.tsx:40 +msgid "Your Role" +msgstr "Your Role" diff --git a/packages/admin/src/routes/users.tsx b/packages/admin/src/routes/users.tsx index c924a1cca..b985b9220 100644 --- a/packages/admin/src/routes/users.tsx +++ b/packages/admin/src/routes/users.tsx @@ -13,7 +13,7 @@ import { UserListSkeleton, UserDetail, InviteUserModal, - getRoleLabel, + useRolesConfig, } from "../components/users"; import { fetchUsers, @@ -41,6 +41,7 @@ function useDebounce(value: T, delay: number): T { } export function UsersPage() { + const { getRoleLabel } = useRolesConfig(); const queryClient = useQueryClient(); // State diff --git a/packages/admin/tests/components/CapabilityConsentDialog.test.tsx b/packages/admin/tests/components/CapabilityConsentDialog.test.tsx index c193ffb82..7b4372607 100644 --- a/packages/admin/tests/components/CapabilityConsentDialog.test.tsx +++ b/packages/admin/tests/components/CapabilityConsentDialog.test.tsx @@ -1,8 +1,8 @@ import * as React from "react"; import { describe, it, expect, vi, beforeEach } from "vitest"; -import { render } from "vitest-browser-react"; import { CapabilityConsentDialog } from "../../src/components/CapabilityConsentDialog"; +import { render } from "../utils/render.tsx"; describe("CapabilityConsentDialog", () => { let onConfirm: ReturnType; diff --git a/packages/admin/tests/components/ContentEditor.test.tsx b/packages/admin/tests/components/ContentEditor.test.tsx index a86719f02..6f2a4b07d 100644 --- a/packages/admin/tests/components/ContentEditor.test.tsx +++ b/packages/admin/tests/components/ContentEditor.test.tsx @@ -1,6 +1,5 @@ import * as React from "react"; import { describe, it, expect, vi, beforeEach } from "vitest"; -import { render } from "vitest-browser-react"; import { ContentEditor, @@ -8,6 +7,7 @@ import { type ContentEditorProps, } from "../../src/components/ContentEditor"; import type { ContentItem } from "../../src/lib/api"; +import { render } from "../utils/render.tsx"; // Mock child components that have complex dependencies vi.mock("../../src/components/PortableTextEditor", () => ({ diff --git a/packages/admin/tests/components/ContentList.test.tsx b/packages/admin/tests/components/ContentList.test.tsx index 6f4b8f2e7..0ddb2b35f 100644 --- a/packages/admin/tests/components/ContentList.test.tsx +++ b/packages/admin/tests/components/ContentList.test.tsx @@ -1,9 +1,9 @@ import * as React from "react"; import { describe, it, expect, vi, beforeEach } from "vitest"; -import { render } from "vitest-browser-react"; import { ContentList } from "../../src/components/ContentList"; import type { ContentItem, TrashedContentItem } from "../../src/lib/api"; +import { render } from "../utils/render.tsx"; const NO_RESULTS_PATTERN = /No results for/; const HAS_MORE_ITEMS_PATTERN = /21\+ items/; diff --git a/packages/admin/tests/components/ContentTypeEditor.test.tsx b/packages/admin/tests/components/ContentTypeEditor.test.tsx index d25cbd5a6..ab799e1da 100644 --- a/packages/admin/tests/components/ContentTypeEditor.test.tsx +++ b/packages/admin/tests/components/ContentTypeEditor.test.tsx @@ -1,12 +1,12 @@ import * as React from "react"; import { describe, it, expect, vi } from "vitest"; -import { render } from "vitest-browser-react"; import { ContentTypeEditor, type ContentTypeEditorProps, } from "../../src/components/ContentTypeEditor"; import type { SchemaCollectionWithFields, SchemaField } from "../../src/lib/api"; +import { render } from "../utils/render"; // Regexes hoisted to module scope to avoid recompilation per call const EDIT_TITLE_RE = /Edit Title field/i; diff --git a/packages/admin/tests/components/ContentTypeList.test.tsx b/packages/admin/tests/components/ContentTypeList.test.tsx index b0241d5b4..c7b407756 100644 --- a/packages/admin/tests/components/ContentTypeList.test.tsx +++ b/packages/admin/tests/components/ContentTypeList.test.tsx @@ -1,9 +1,9 @@ import * as React from "react"; import { describe, it, expect, vi, beforeEach } from "vitest"; -import { render } from "vitest-browser-react"; import { ContentTypeList } from "../../src/components/ContentTypeList"; import type { SchemaCollection, OrphanedTable } from "../../src/lib/api"; +import { render } from "../utils/render.tsx"; // --------------------------------------------------------------------------- // Constants diff --git a/packages/admin/tests/components/FieldEditor.test.tsx b/packages/admin/tests/components/FieldEditor.test.tsx index 54df3b6a0..0a5f9497d 100644 --- a/packages/admin/tests/components/FieldEditor.test.tsx +++ b/packages/admin/tests/components/FieldEditor.test.tsx @@ -1,9 +1,9 @@ import * as React from "react"; import { describe, it, expect, vi, beforeEach } from "vitest"; -import { render } from "vitest-browser-react"; import { FieldEditor } from "../../src/components/FieldEditor"; import type { SchemaField } from "../../src/lib/api"; +import { render } from "../utils/render.tsx"; // --------------------------------------------------------------------------- // Constants diff --git a/packages/admin/tests/components/Header.test.tsx b/packages/admin/tests/components/Header.test.tsx index 473a0bfdf..8ea9a1bab 100644 --- a/packages/admin/tests/components/Header.test.tsx +++ b/packages/admin/tests/components/Header.test.tsx @@ -2,9 +2,9 @@ import { Sidebar } from "@cloudflare/kumo"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import * as React from "react"; import { describe, it, expect, vi, beforeEach } from "vitest"; -import { render } from "vitest-browser-react"; import { ThemeProvider } from "../../src/components/ThemeProvider"; +import { render } from "../utils/render.tsx"; // Mock router vi.mock("@tanstack/react-router", async () => { diff --git a/packages/admin/tests/components/LoginPage.test.tsx b/packages/admin/tests/components/LoginPage.test.tsx index ee329c375..bf609c654 100644 --- a/packages/admin/tests/components/LoginPage.test.tsx +++ b/packages/admin/tests/components/LoginPage.test.tsx @@ -2,7 +2,7 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import * as React from "react"; import { describe, it, expect, vi, beforeEach } from "vitest"; -import { render } from "../utils/render.js"; +import { render } from "../utils/render.tsx"; // Mock router vi.mock("@tanstack/react-router", async () => { diff --git a/packages/admin/tests/components/MarketplaceBrowse.test.tsx b/packages/admin/tests/components/MarketplaceBrowse.test.tsx index 08bd19211..d74069061 100644 --- a/packages/admin/tests/components/MarketplaceBrowse.test.tsx +++ b/packages/admin/tests/components/MarketplaceBrowse.test.tsx @@ -1,12 +1,12 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import * as React from "react"; import { describe, it, expect, vi, beforeEach } from "vitest"; -import { render } from "vitest-browser-react"; import type { MarketplaceSearchResult, MarketplacePluginSummary, } from "../../src/lib/api/marketplace"; +import { render } from "../utils/render.tsx"; // Mock router const mockNavigate = vi.fn(); diff --git a/packages/admin/tests/components/MarketplacePluginDetail.test.tsx b/packages/admin/tests/components/MarketplacePluginDetail.test.tsx index 0cf218eaa..26a32f016 100644 --- a/packages/admin/tests/components/MarketplacePluginDetail.test.tsx +++ b/packages/admin/tests/components/MarketplacePluginDetail.test.tsx @@ -1,9 +1,9 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import * as React from "react"; import { describe, it, expect, vi, beforeEach } from "vitest"; -import { render } from "vitest-browser-react"; import type { MarketplacePluginDetail as PluginDetailType } from "../../src/lib/api/marketplace"; +import { render } from "../utils/render.tsx"; const INSTALL_RE = /Install/; diff --git a/packages/admin/tests/components/MediaDetailPanel.test.tsx b/packages/admin/tests/components/MediaDetailPanel.test.tsx index 1e874211d..d76572361 100644 --- a/packages/admin/tests/components/MediaDetailPanel.test.tsx +++ b/packages/admin/tests/components/MediaDetailPanel.test.tsx @@ -1,10 +1,10 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import * as React from "react"; import { describe, it, expect, vi, beforeEach } from "vitest"; -import { render } from "vitest-browser-react"; import { MediaDetailPanel } from "../../src/components/MediaDetailPanel"; import type { MediaItem } from "../../src/lib/api"; +import { render } from "../utils/render.tsx"; vi.mock("../../src/lib/api", async () => { const actual = await vi.importActual("../../src/lib/api"); diff --git a/packages/admin/tests/components/MediaLibrary.test.tsx b/packages/admin/tests/components/MediaLibrary.test.tsx index 9a76c6512..01b6f5a15 100644 --- a/packages/admin/tests/components/MediaLibrary.test.tsx +++ b/packages/admin/tests/components/MediaLibrary.test.tsx @@ -1,10 +1,10 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import * as React from "react"; import { describe, it, expect, vi, beforeEach } from "vitest"; -import { render } from "vitest-browser-react"; import { MediaLibrary } from "../../src/components/MediaLibrary"; import type { MediaItem } from "../../src/lib/api"; +import { render } from "../utils/render.tsx"; // --------------------------------------------------------------------------- // Constants diff --git a/packages/admin/tests/components/MediaPickerModal.test.tsx b/packages/admin/tests/components/MediaPickerModal.test.tsx index 0a6c69ca2..abc30a593 100644 --- a/packages/admin/tests/components/MediaPickerModal.test.tsx +++ b/packages/admin/tests/components/MediaPickerModal.test.tsx @@ -1,9 +1,9 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import * as React from "react"; import { describe, it, expect, vi, beforeEach } from "vitest"; -import { render } from "vitest-browser-react"; import { MediaPickerModal } from "../../src/components/MediaPickerModal"; +import { render } from "../utils/render.tsx"; // --------------------------------------------------------------------------- // Constants diff --git a/packages/admin/tests/components/MenuEditor.test.tsx b/packages/admin/tests/components/MenuEditor.test.tsx index 0cde26c41..78e72960c 100644 --- a/packages/admin/tests/components/MenuEditor.test.tsx +++ b/packages/admin/tests/components/MenuEditor.test.tsx @@ -2,9 +2,9 @@ import { Toasty } from "@cloudflare/kumo"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import * as React from "react"; import { describe, it, expect, vi, beforeEach } from "vitest"; -import { render } from "vitest-browser-react"; import { MenuEditor } from "../../src/components/MenuEditor"; +import { render } from "../utils/render.tsx"; vi.mock("@tanstack/react-router", async () => { const actual = await vi.importActual("@tanstack/react-router"); diff --git a/packages/admin/tests/components/MenuList.test.tsx b/packages/admin/tests/components/MenuList.test.tsx index 8ead5c146..dd51b5e03 100644 --- a/packages/admin/tests/components/MenuList.test.tsx +++ b/packages/admin/tests/components/MenuList.test.tsx @@ -2,9 +2,9 @@ import { Toasty } from "@cloudflare/kumo"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import * as React from "react"; import { describe, it, expect, vi, beforeEach } from "vitest"; -import { render } from "vitest-browser-react"; import { MenuList } from "../../src/components/MenuList"; +import { render } from "../utils/render.tsx"; vi.mock("@tanstack/react-router", async () => { const actual = await vi.importActual("@tanstack/react-router"); diff --git a/packages/admin/tests/components/PluginManager.test.tsx b/packages/admin/tests/components/PluginManager.test.tsx index ade35019f..294ead254 100644 --- a/packages/admin/tests/components/PluginManager.test.tsx +++ b/packages/admin/tests/components/PluginManager.test.tsx @@ -2,10 +2,10 @@ import { Toasty } from "@cloudflare/kumo"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import * as React from "react"; import { describe, it, expect, vi, beforeEach } from "vitest"; -import { render } from "vitest-browser-react"; import type { PluginInfo, AdminManifest } from "../../src/lib/api"; import type { PluginUpdateInfo } from "../../src/lib/api/marketplace"; +import { render } from "../utils/render.tsx"; // Mock router vi.mock("@tanstack/react-router", async () => { diff --git a/packages/admin/tests/components/RevisionHistory.test.tsx b/packages/admin/tests/components/RevisionHistory.test.tsx index d5872b753..f98488576 100644 --- a/packages/admin/tests/components/RevisionHistory.test.tsx +++ b/packages/admin/tests/components/RevisionHistory.test.tsx @@ -3,10 +3,10 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { userEvent } from "@vitest/browser/context"; import * as React from "react"; import { describe, it, expect, vi, beforeEach, type Mock } from "vitest"; -import { render } from "vitest-browser-react"; import { RevisionHistory } from "../../src/components/RevisionHistory"; import type { Revision, RevisionListResponse } from "../../src/lib/api"; +import { render } from "../utils/render.tsx"; // Mock the API module vi.mock("../../src/lib/api", async () => { diff --git a/packages/admin/tests/components/SaveButton.test.tsx b/packages/admin/tests/components/SaveButton.test.tsx index 75f959abd..1770feb6b 100644 --- a/packages/admin/tests/components/SaveButton.test.tsx +++ b/packages/admin/tests/components/SaveButton.test.tsx @@ -1,8 +1,8 @@ import * as React from "react"; import { describe, it, expect } from "vitest"; -import { render } from "vitest-browser-react"; import { SaveButton } from "../../src/components/SaveButton"; +import { render } from "../utils/render.tsx"; describe("SaveButton", () => { it("shows 'Save' when dirty and not saving", async () => { diff --git a/packages/admin/tests/components/SectionPickerModal.test.tsx b/packages/admin/tests/components/SectionPickerModal.test.tsx index 9c75662f1..a3b47ee04 100644 --- a/packages/admin/tests/components/SectionPickerModal.test.tsx +++ b/packages/admin/tests/components/SectionPickerModal.test.tsx @@ -1,9 +1,9 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import * as React from "react"; import { describe, it, expect, vi, beforeEach } from "vitest"; -import { render } from "vitest-browser-react"; import type { Section, SectionCategory, SectionsResult } from "../../src/lib/api"; +import { render } from "../utils/render.tsx"; const mockFetchSections = vi.fn<() => Promise>(); const mockFetchSectionCategories = vi.fn<() => Promise>(); diff --git a/packages/admin/tests/components/Sections.test.tsx b/packages/admin/tests/components/Sections.test.tsx index aacd04705..54f372969 100644 --- a/packages/admin/tests/components/Sections.test.tsx +++ b/packages/admin/tests/components/Sections.test.tsx @@ -2,9 +2,9 @@ import { Toasty } from "@cloudflare/kumo"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import * as React from "react"; import { describe, it, expect, vi, beforeEach } from "vitest"; -import { render } from "vitest-browser-react"; import type { Section, SectionsResult } from "../../src/lib/api"; +import { render } from "../utils/render.tsx"; // Mock router vi.mock("@tanstack/react-router", async () => { diff --git a/packages/admin/tests/components/Settings.test.tsx b/packages/admin/tests/components/Settings.test.tsx index 0fce6890b..7ee161a58 100644 --- a/packages/admin/tests/components/Settings.test.tsx +++ b/packages/admin/tests/components/Settings.test.tsx @@ -3,7 +3,7 @@ import * as React from "react"; import { describe, it, expect, vi, beforeEach } from "vitest"; import type { AdminManifest } from "../../src/lib/api"; -import { render } from "../utils/render.js"; +import { render } from "../utils/render.tsx"; // Mock router vi.mock("@tanstack/react-router", async () => { diff --git a/packages/admin/tests/components/SetupWizard.test.tsx b/packages/admin/tests/components/SetupWizard.test.tsx index e4012d806..08e0150cf 100644 --- a/packages/admin/tests/components/SetupWizard.test.tsx +++ b/packages/admin/tests/components/SetupWizard.test.tsx @@ -1,7 +1,8 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import * as React from "react"; import { describe, it, expect, vi, beforeEach } from "vitest"; -import { render } from "vitest-browser-react"; + +import { render } from "../utils/render.tsx"; // Mock API vi.mock("../../src/lib/api/client", async () => { diff --git a/packages/admin/tests/components/SignupPage.test.tsx b/packages/admin/tests/components/SignupPage.test.tsx index 32540b951..9e8657e28 100644 --- a/packages/admin/tests/components/SignupPage.test.tsx +++ b/packages/admin/tests/components/SignupPage.test.tsx @@ -1,6 +1,7 @@ import * as React from "react"; import { describe, it, expect, vi, beforeEach } from "vitest"; -import { render } from "vitest-browser-react"; + +import { render } from "../utils/render.tsx"; // Mock router vi.mock("@tanstack/react-router", async () => { diff --git a/packages/admin/tests/components/TaxonomyManager.test.tsx b/packages/admin/tests/components/TaxonomyManager.test.tsx index 49cf5a617..a915f0619 100644 --- a/packages/admin/tests/components/TaxonomyManager.test.tsx +++ b/packages/admin/tests/components/TaxonomyManager.test.tsx @@ -2,9 +2,9 @@ import { Toasty } from "@cloudflare/kumo"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import * as React from "react"; import { describe, it, expect, vi, beforeEach } from "vitest"; -import { render } from "vitest-browser-react"; import { TaxonomyManager } from "../../src/components/TaxonomyManager"; +import { render } from "../utils/render.tsx"; const taxonomyResponse = JSON.stringify({ data: { diff --git a/packages/admin/tests/components/ThemeToggle.test.tsx b/packages/admin/tests/components/ThemeToggle.test.tsx index 57c33abb0..b9c7b3e38 100644 --- a/packages/admin/tests/components/ThemeToggle.test.tsx +++ b/packages/admin/tests/components/ThemeToggle.test.tsx @@ -1,9 +1,9 @@ import * as React from "react"; import { describe, it, expect, beforeEach } from "vitest"; -import { render } from "vitest-browser-react"; import { ThemeProvider } from "../../src/components/ThemeProvider"; import { ThemeToggle } from "../../src/components/ThemeToggle"; +import { render } from "../utils/render.tsx"; function TestThemeToggle({ defaultTheme = "system" as "system" | "light" | "dark" }) { return ( diff --git a/packages/admin/tests/components/WelcomeModal.test.tsx b/packages/admin/tests/components/WelcomeModal.test.tsx index 0bc8c66bb..c69b911d2 100644 --- a/packages/admin/tests/components/WelcomeModal.test.tsx +++ b/packages/admin/tests/components/WelcomeModal.test.tsx @@ -1,9 +1,9 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import * as React from "react"; import { describe, it, expect, vi } from "vitest"; -import { render } from "vitest-browser-react"; import { WelcomeModal } from "../../src/components/WelcomeModal"; +import { render } from "../utils/render"; // --------------------------------------------------------------------------- // Constants diff --git a/packages/admin/tests/components/Widgets.test.tsx b/packages/admin/tests/components/Widgets.test.tsx index 9af224e45..4389035e5 100644 --- a/packages/admin/tests/components/Widgets.test.tsx +++ b/packages/admin/tests/components/Widgets.test.tsx @@ -2,9 +2,9 @@ import { Toasty } from "@cloudflare/kumo"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import * as React from "react"; import { describe, it, expect, vi, beforeEach } from "vitest"; -import { render } from "vitest-browser-react"; import { Widgets } from "../../src/components/Widgets"; +import { render } from "../utils/render"; vi.mock("../../src/lib/api", async () => { const actual = await vi.importActual("../../src/lib/api"); diff --git a/packages/admin/tests/components/settings/AllowedDomainsSettings.test.tsx b/packages/admin/tests/components/settings/AllowedDomainsSettings.test.tsx index 9b8484545..1dd81a448 100644 --- a/packages/admin/tests/components/settings/AllowedDomainsSettings.test.tsx +++ b/packages/admin/tests/components/settings/AllowedDomainsSettings.test.tsx @@ -2,9 +2,9 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { userEvent } from "@vitest/browser/context"; import * as React from "react"; import { describe, it, expect, vi, beforeEach } from "vitest"; -import { render } from "vitest-browser-react"; import { AllowedDomainsSettings } from "../../../src/components/settings/AllowedDomainsSettings"; +import { render } from "../../utils/render"; vi.mock("@tanstack/react-router", async () => { const actual = await vi.importActual("@tanstack/react-router"); diff --git a/packages/admin/tests/components/users/InviteUserModal.test.tsx b/packages/admin/tests/components/users/InviteUserModal.test.tsx index 66fdcc200..35665a115 100644 --- a/packages/admin/tests/components/users/InviteUserModal.test.tsx +++ b/packages/admin/tests/components/users/InviteUserModal.test.tsx @@ -1,9 +1,9 @@ import { userEvent } from "@vitest/browser/context"; import * as React from "react"; import { describe, it, expect, vi } from "vitest"; -import { render } from "vitest-browser-react"; import { InviteUserModal } from "../../../src/components/users/InviteUserModal"; +import { render } from "../../utils/render"; const noop = () => {}; diff --git a/packages/admin/tests/components/users/UserDetail.test.tsx b/packages/admin/tests/components/users/UserDetail.test.tsx index c515b3c10..865877d9b 100644 --- a/packages/admin/tests/components/users/UserDetail.test.tsx +++ b/packages/admin/tests/components/users/UserDetail.test.tsx @@ -1,10 +1,10 @@ import { userEvent } from "@vitest/browser/context"; import * as React from "react"; import { describe, it, expect, vi } from "vitest"; -import { render } from "vitest-browser-react"; import { UserDetail } from "../../../src/components/users/UserDetail"; import type { UserDetail as UserDetailType } from "../../../src/lib/api"; +import { render } from "../../utils/render"; function makeUser(overrides: Partial = {}): UserDetailType { return { diff --git a/packages/admin/tests/editor-focus.test.tsx b/packages/admin/tests/editor-focus.test.tsx index e9aa9e3ab..5f9dec3c8 100644 --- a/packages/admin/tests/editor-focus.test.tsx +++ b/packages/admin/tests/editor-focus.test.tsx @@ -1,10 +1,12 @@ -import { render, screen } from "@testing-library/react"; +import { screen } from "@testing-library/react"; import Focus from "@tiptap/extension-focus"; import { useEditor, EditorContent } from "@tiptap/react"; import StarterKit from "@tiptap/starter-kit"; import * as React from "react"; import { describe, it, expect, vi } from "vitest"; +import { render } from "./utils/render.js"; + // Test wrapper to render editor with Focus extension function TestEditor({ spotlightMode = false }: { spotlightMode?: boolean }) { const editor = useEditor({ @@ -44,7 +46,7 @@ function TestEditor({ spotlightMode = false }: { spotlightMode?: boolean }) { describe("Editor Focus Mode", () => { it("Focus extension is configured correctly", async () => { - render(); + void render(); // Wait for editor to initialize (not just loading state) await vi.waitFor( @@ -68,7 +70,7 @@ describe("Editor Focus Mode", () => { }); it("spotlight mode applies CSS class to editor wrapper", async () => { - render(); + void render(); // Wait for editor to initialize await vi.waitFor( @@ -84,7 +86,7 @@ describe("Editor Focus Mode", () => { }); it("non-spotlight mode does not have spotlight-mode class", async () => { - render(); + void render(); // Wait for editor to initialize await vi.waitFor( diff --git a/packages/admin/tests/editor/PortableTextEditor.test.tsx b/packages/admin/tests/editor/PortableTextEditor.test.tsx index a3b91209a..7a2f54d7a 100644 --- a/packages/admin/tests/editor/PortableTextEditor.test.tsx +++ b/packages/admin/tests/editor/PortableTextEditor.test.tsx @@ -9,10 +9,10 @@ import type { Editor } from "@tiptap/react"; import * as React from "react"; import { describe, it, expect, vi } from "vitest"; -import { render } from "vitest-browser-react"; import type { PluginBlockDef } from "../../src/components/PortableTextEditor"; import { PortableTextEditor } from "../../src/components/PortableTextEditor"; +import { render } from "../utils/render"; // --------------------------------------------------------------------------- // Mocks — heavy components that need network / Astro context diff --git a/packages/admin/tests/editor/block-menu.test.tsx b/packages/admin/tests/editor/block-menu.test.tsx index 116a3d4fa..cc5ba5564 100644 --- a/packages/admin/tests/editor/block-menu.test.tsx +++ b/packages/admin/tests/editor/block-menu.test.tsx @@ -15,10 +15,10 @@ import type { Editor } from "@tiptap/react"; import { userEvent } from "@vitest/browser/context"; import * as React from "react"; import { describe, it, expect, vi } from "vitest"; -import { render } from "vitest-browser-react"; import { BlockMenu } from "../../src/components/editor/BlockMenu"; import { PortableTextEditor } from "../../src/components/PortableTextEditor"; +import { render } from "../utils/render"; // --------------------------------------------------------------------------- // Mocks — same as other editor tests diff --git a/packages/admin/tests/editor/bubble-menu.test.tsx b/packages/admin/tests/editor/bubble-menu.test.tsx index 91e172649..505fbc2f9 100644 --- a/packages/admin/tests/editor/bubble-menu.test.tsx +++ b/packages/admin/tests/editor/bubble-menu.test.tsx @@ -12,10 +12,10 @@ import type { Editor } from "@tiptap/react"; import { userEvent } from "@vitest/browser/context"; import { describe, it, expect, vi } from "vitest"; -import { render } from "vitest-browser-react"; import type { PortableTextEditorProps } from "../../src/components/PortableTextEditor"; import { PortableTextEditor } from "../../src/components/PortableTextEditor"; +import { render } from "../utils/render.tsx"; // --------------------------------------------------------------------------- // Mocks diff --git a/packages/admin/tests/editor/slash-menu.test.tsx b/packages/admin/tests/editor/slash-menu.test.tsx index 07d0af30f..314f48059 100644 --- a/packages/admin/tests/editor/slash-menu.test.tsx +++ b/packages/admin/tests/editor/slash-menu.test.tsx @@ -12,10 +12,10 @@ import type { Editor } from "@tiptap/react"; import { userEvent } from "@vitest/browser/context"; import { describe, it, expect, vi } from "vitest"; -import { render } from "vitest-browser-react"; import type { PortableTextEditorProps } from "../../src/components/PortableTextEditor"; import { PortableTextEditor } from "../../src/components/PortableTextEditor"; +import { render } from "../utils/render"; // --------------------------------------------------------------------------- // Mocks diff --git a/packages/admin/tests/editor/toolbar.test.tsx b/packages/admin/tests/editor/toolbar.test.tsx index beca90966..b63390f1e 100644 --- a/packages/admin/tests/editor/toolbar.test.tsx +++ b/packages/admin/tests/editor/toolbar.test.tsx @@ -1,12 +1,12 @@ import type { Editor } from "@tiptap/core"; import { userEvent } from "@vitest/browser/context"; import { describe, it, expect, vi } from "vitest"; -import { render } from "vitest-browser-react"; import { PortableTextEditor, type PortableTextEditorProps, } from "../../src/components/PortableTextEditor"; +import { render } from "../utils/render.tsx"; // --------------------------------------------------------------------------- // Mocks — heavy components that need network / Astro context diff --git a/packages/admin/tests/lib/api-token-scopes-contract.test.ts b/packages/admin/tests/lib/api-token-scopes-contract.test.ts new file mode 100644 index 000000000..89c368364 --- /dev/null +++ b/packages/admin/tests/lib/api-token-scopes-contract.test.ts @@ -0,0 +1,22 @@ +import { describe, expect, it } from "vitest"; + +// Import source (not package `dist`) so the contract tracks edits without a prior `auth` build. +import { VALID_SCOPES } from "../../../auth/src/tokens.js"; +import { API_TOKEN_SCOPE_FORM_SCOPES } from "../../src/components/settings/ApiTokenSettings.js"; +import { API_TOKEN_SCOPES } from "../../src/lib/api/api-tokens.js"; + +function sortedUnique(values: readonly string[]): string[] { + return [...new Set(values)].toSorted((a, b) => a.localeCompare(b)); +} + +describe("API token scope drift guard", () => { + it("admin wire constants match server VALID_SCOPES", () => { + expect(sortedUnique(Object.values(API_TOKEN_SCOPES))).toEqual(sortedUnique(VALID_SCOPES)); + }); + + it("create-token UI lists the same scopes as API_TOKEN_SCOPES", () => { + expect(sortedUnique(API_TOKEN_SCOPE_FORM_SCOPES)).toEqual( + sortedUnique(Object.values(API_TOKEN_SCOPES)), + ); + }); +}); diff --git a/packages/admin/tests/lib/hooks.test.tsx b/packages/admin/tests/lib/hooks.test.tsx index e63e96569..b2cd7a688 100644 --- a/packages/admin/tests/lib/hooks.test.tsx +++ b/packages/admin/tests/lib/hooks.test.tsx @@ -1,9 +1,9 @@ import { userEvent } from "@vitest/browser/context"; import * as React from "react"; import { describe, it, expect, vi } from "vitest"; -import { render } from "vitest-browser-react"; import { useStableCallback } from "../../src/lib/hooks"; +import { render } from "../utils/render.tsx"; /** * Test component that attaches a keydown listener using useStableCallback. diff --git a/packages/admin/tests/router.test.tsx b/packages/admin/tests/router.test.tsx index 0896478de..7197b6a3f 100644 --- a/packages/admin/tests/router.test.tsx +++ b/packages/admin/tests/router.test.tsx @@ -24,10 +24,10 @@ import { QueryClientProvider } from "@tanstack/react-query"; import { RouterProvider } from "@tanstack/react-router"; import * as React from "react"; import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; -import { render } from "vitest-browser-react"; import type { AdminManifest } from "../src/lib/api"; import { createAdminRouter } from "../src/router"; +import { render } from "./utils/render.tsx"; import { createTestQueryClient, createMockFetch } from "./utils/test-helpers"; // --------------------------------------------------------------------------- diff --git a/packages/admin/tests/setup.ts b/packages/admin/tests/setup.ts index e9703402d..60b705283 100644 --- a/packages/admin/tests/setup.ts +++ b/packages/admin/tests/setup.ts @@ -1,6 +1,5 @@ -import { i18n } from "@lingui/core"; import "vitest-browser-react"; +import { i18n } from "@lingui/core"; -if (!i18n.locale) { - i18n.loadAndActivate({ locale: "en", messages: {} }); -} +// Initialize i18n for browser tests with empty English messages +i18n.loadAndActivate({ locale: "en", messages: {} }); diff --git a/packages/admin/tests/utils/render.tsx b/packages/admin/tests/utils/render.tsx index 4725f72b6..c55101782 100644 --- a/packages/admin/tests/utils/render.tsx +++ b/packages/admin/tests/utils/render.tsx @@ -3,10 +3,6 @@ import { I18nProvider } from "@lingui/react"; import * as React from "react"; import { render as baseRender, type ComponentRenderOptions } from "vitest-browser-react"; -if (!i18n.locale) { - i18n.loadAndActivate({ locale: "en", messages: {} }); -} - type RenderWrapper = ComponentRenderOptions["wrapper"]; const I18nWrapper = (InnerWrapper: RenderWrapper = React.Fragment) => {