From 4554ececddb9df22e7f7f2ab5035fa40d1a46d0b Mon Sep 17 00:00:00 2001 From: Ophir Bucai Date: Sun, 12 Apr 2026 01:46:34 +0300 Subject: [PATCH 01/48] Add i18n pre-initialization to fix module-level macro timing Creates a dedicated init module that pre-initializes Lingui's i18n instance with English locale before any other modules execute. This prevents "no locale set" errors when module-level t`` macros evaluate. The init module is imported in both the admin package entry point and the Astro admin route to ensure early initialization regardless of Astro's island hydration order. Added src/locales/init.ts to tsdown entry array so it builds to dist/locales/init.js. --- packages/admin/src/index.ts | 6 ++++++ packages/admin/src/locales/init.ts | 14 ++++++++++++++ packages/admin/tsdown.config.ts | 2 +- packages/core/src/astro/routes/admin.astro | 2 ++ 4 files changed, 23 insertions(+), 1 deletion(-) create mode 100644 packages/admin/src/locales/init.ts diff --git a/packages/admin/src/index.ts b/packages/admin/src/index.ts index aadc83fde..95dc765fb 100644 --- a/packages/admin/src/index.ts +++ b/packages/admin/src/index.ts @@ -1,3 +1,9 @@ +/** + * CRITICAL: This must be imported FIRST to pre-initialize i18n + * before any module-level t`...` calls execute. + */ +import "./locales/init.js"; + // Main App export { AdminApp, default as App } from "./App"; diff --git a/packages/admin/src/locales/init.ts b/packages/admin/src/locales/init.ts new file mode 100644 index 000000000..487adf1da --- /dev/null +++ b/packages/admin/src/locales/init.ts @@ -0,0 +1,14 @@ +/** + * Pre-initialize i18n with English locale. + * + * This MUST be imported first in index.ts to ensure i18n is ready + * before any other module executes module-level t`...` calls. + * + * Side-effect import - modifies global i18n instance. + */ +import { i18n } from "@lingui/core"; +import { messages } from "./en/messages.mjs"; + +if (!i18n.locale) { + i18n.loadAndActivate({ locale: "en", messages }); +} diff --git a/packages/admin/tsdown.config.ts b/packages/admin/tsdown.config.ts index 510ae580e..52fbc0939 100644 --- a/packages/admin/tsdown.config.ts +++ b/packages/admin/tsdown.config.ts @@ -24,7 +24,7 @@ function linguiMacroPlugin(): Plugin { } export default defineConfig({ - entry: ["src/index.ts", "src/locales/index.ts"], + entry: ["src/index.ts", "src/locales/index.ts", "src/locales/init.ts"], format: ["esm"], dts: true, clean: true, diff --git a/packages/core/src/astro/routes/admin.astro b/packages/core/src/astro/routes/admin.astro index bcdbd9e49..ac93495fd 100644 --- a/packages/core/src/astro/routes/admin.astro +++ b/packages/core/src/astro/routes/admin.astro @@ -6,6 +6,8 @@ * AdminWrapper imports plugin admin modules and passes them to AdminApp. */ import "@emdash-cms/admin/styles.css"; +// Pre-initialize i18n before any client modules load +import "@emdash-cms/admin/locales/init"; // Use package-qualified import so Astro generates a proper module URL // (relative imports resolve to absolute paths which break client hydration) import AdminWrapper from "emdash/routes/PluginRegistry"; From 8ebe6a3bd27f7ebbd6cb9d40a241480bf171631d Mon Sep 17 00:00:00 2001 From: Ophir Bucai Date: Sun, 12 Apr 2026 01:46:45 +0300 Subject: [PATCH 02/48] Apply Lingui t\`\` macro to module-level strings Wraps user-facing strings in module-level constants and helper functions with the t\`\` macro from @lingui/core/macro. These strings are defined outside React components but are called lazily within useMemo hooks or during render, ensuring they evaluate after i18n initialization. Changes include: - AdminCommandPalette: buildNavItems function - PortableTextEditor: block type definitions and embed config - BlockMenu, Widgets: block/widget labels and descriptions - ContentTypeEditor: field labels and descriptions - ApiTokenSettings: expiry options - api-tokens.ts: scope labels and descriptions - RoleBadge: role names and descriptions - WelcomeModal: role names - AllowedDomainsSettings: role names - MediaPickerModal, MediaLibrary: tab labels All wrapped strings will be extracted by lingui extract in the next commit. Components using these strings have i18n.locale in their useMemo dependencies to trigger re-render on locale change. --- .../src/components/AdminCommandPalette.tsx | 37 +++++++------ .../src/components/ContentTypeEditor.tsx | 41 +++++++------- .../admin/src/components/MediaLibrary.tsx | 6 ++- .../admin/src/components/MediaPickerModal.tsx | 6 ++- .../src/components/PortableTextEditor.tsx | 53 ++++++++++--------- .../admin/src/components/WelcomeModal.tsx | 11 ++-- packages/admin/src/components/Widgets.tsx | 13 ++--- .../admin/src/components/editor/BlockMenu.tsx | 17 +++--- .../settings/AllowedDomainsSettings.tsx | 11 ++-- .../components/settings/ApiTokenSettings.tsx | 11 ++-- .../admin/src/components/users/RoleBadge.tsx | 25 ++++----- packages/admin/src/lib/api/api-tokens.ts | 15 +++--- 12 files changed, 134 insertions(+), 112 deletions(-) diff --git a/packages/admin/src/components/AdminCommandPalette.tsx b/packages/admin/src/components/AdminCommandPalette.tsx index 3ccf5b7fc..203b096da 100644 --- a/packages/admin/src/components/AdminCommandPalette.tsx +++ b/packages/admin/src/components/AdminCommandPalette.tsx @@ -5,6 +5,8 @@ * Opens with Cmd+K (Mac) or Ctrl+K (Windows/Linux). */ +import { t } from "@lingui/core/macro"; +import { useLingui } from "@lingui/react/macro"; import { CommandPalette } from "@cloudflare/kumo"; import { SquaresFour, @@ -134,7 +136,7 @@ function buildNavItems( const items: NavItem[] = [ { id: "dashboard", - title: "Dashboard", + title: t`Dashboard`, to: "/", icon: SquaresFour, keywords: ["home", "overview"], @@ -157,14 +159,14 @@ function buildNavItems( items.push( { id: "media", - title: "Media Library", + title: t`Media Library`, to: "/media", icon: Image, keywords: ["images", "files", "uploads"], }, { id: "menus", - title: "Menus", + title: t`Menus`, to: "/menus", icon: List, minRole: ROLE_EDITOR, @@ -172,7 +174,7 @@ function buildNavItems( }, { id: "widgets", - title: "Widgets", + title: t`Widgets`, to: "/widgets", icon: GridFour, minRole: ROLE_EDITOR, @@ -180,7 +182,7 @@ function buildNavItems( }, { id: "sections", - title: "Sections", + title: t`Sections`, to: "/sections", icon: Stack, minRole: ROLE_EDITOR, @@ -188,7 +190,7 @@ function buildNavItems( }, { id: "content-types", - title: "Content Types", + title: t`Content Types`, to: "/content-types", icon: Database, minRole: ROLE_ADMIN, @@ -196,7 +198,7 @@ function buildNavItems( }, { id: "categories", - title: "Categories", + title: t`Categories`, to: "/taxonomies/$taxonomy", params: { taxonomy: "category" }, icon: FileText, @@ -205,7 +207,7 @@ function buildNavItems( }, { id: "tags", - title: "Tags", + title: t`Tags`, to: "/taxonomies/$taxonomy", params: { taxonomy: "tag" }, icon: FileText, @@ -214,7 +216,7 @@ function buildNavItems( }, { id: "users", - title: "Users", + title: t`Users`, to: "/users", icon: Users, minRole: ROLE_ADMIN, @@ -222,7 +224,7 @@ function buildNavItems( }, { id: "plugins", - title: "Plugins", + title: t`Plugins`, to: "/plugins-manager", icon: PuzzlePiece, minRole: ROLE_ADMIN, @@ -230,7 +232,7 @@ function buildNavItems( }, { id: "import", - title: "Import", + title: t`Import`, to: "/import/wordpress", icon: Upload, minRole: ROLE_ADMIN, @@ -238,7 +240,7 @@ function buildNavItems( }, { id: "settings", - title: "Settings", + title: t`Settings`, to: "/settings", icon: Gear, minRole: ROLE_ADMIN, @@ -246,7 +248,7 @@ function buildNavItems( }, { id: "security", - title: "Security Settings", + title: t`Security Settings`, to: "/settings/security", icon: Gear, minRole: ROLE_ADMIN, @@ -295,6 +297,7 @@ export function AdminCommandPalette({ manifest }: AdminCommandPaletteProps) { const [open, setOpen] = React.useState(false); const [query, setQuery] = React.useState(""); const navigate = useNavigate(); + const { i18n } = useLingui(); // Debounce the search query to avoid flickering on every keystroke const debouncedQuery = useDebouncedValue(query, SEARCH_DEBOUNCE_MS); @@ -315,8 +318,12 @@ export function AdminCommandPalette({ manifest }: AdminCommandPaletteProps) { const isWaitingForDebounce = query.length >= 2 && query !== debouncedQuery; const isPendingSearch = isWaitingForDebounce || isSearching; - // Build navigation items - const allNavItems = React.useMemo(() => buildNavItems(manifest, userRole), [manifest, userRole]); + // Build navigation items — i18n.locale in deps so items rebuild on locale change + const allNavItems = React.useMemo( + () => buildNavItems(manifest, userRole), + // eslint-disable-next-line react-hooks/exhaustive-deps + [manifest, userRole, i18n.locale], + ); // Filter nav items based on query const filteredNavItems = React.useMemo( diff --git a/packages/admin/src/components/ContentTypeEditor.tsx b/packages/admin/src/components/ContentTypeEditor.tsx index d020ad9eb..1ede1b98d 100644 --- a/packages/admin/src/components/ContentTypeEditor.tsx +++ b/packages/admin/src/components/ContentTypeEditor.tsx @@ -1,3 +1,4 @@ +import { t } from "@lingui/core/macro"; import { Badge, Button, Input, InputArea, Label, Select, buttonVariants } from "@cloudflare/kumo"; import { DndContext, @@ -57,23 +58,23 @@ export interface ContentTypeEditorProps { const SUPPORT_OPTIONS = [ { value: "drafts", - label: "Drafts", - description: "Save content as draft before publishing", + label: t`Drafts`, + description: t`Save content as draft before publishing`, }, { value: "revisions", - label: "Revisions", - description: "Track content history", + label: t`Revisions`, + description: t`Track content history`, }, { value: "preview", - label: "Preview", - description: "Preview content before publishing", + label: t`Preview`, + description: t`Preview content before publishing`, }, { value: "search", - label: "Search", - description: "Enable full-text search on this collection", + label: t`Search`, + description: t`Enable full-text search on this collection`, }, ]; @@ -84,39 +85,39 @@ const SUPPORT_OPTIONS = [ const SYSTEM_FIELDS = [ { slug: "id", - label: "ID", + label: t`ID`, type: "text", - description: "Unique identifier (ULID)", + description: t`Unique identifier (ULID)`, }, { slug: "slug", - label: "Slug", + label: t`Slug`, type: "text", - description: "URL-friendly identifier", + description: t`URL-friendly identifier`, }, { slug: "status", - label: "Status", + label: t`Status`, type: "text", - description: "draft, published, or archived", + description: t`draft, published, or archived`, }, { slug: "created_at", - label: "Created At", + label: t`Created At`, type: "datetime", - description: "When the entry was created", + description: t`When the entry was created`, }, { slug: "updated_at", - label: "Updated At", + label: t`Updated At`, type: "datetime", - description: "When the entry was last modified", + description: t`When the entry was last modified`, }, { slug: "published_at", - label: "Published At", + label: t`Published At`, type: "datetime", - description: "When the entry was published", + description: t`When the entry was published`, }, ]; diff --git a/packages/admin/src/components/MediaLibrary.tsx b/packages/admin/src/components/MediaLibrary.tsx index 76f5bca19..92d490aa7 100644 --- a/packages/admin/src/components/MediaLibrary.tsx +++ b/packages/admin/src/components/MediaLibrary.tsx @@ -1,3 +1,4 @@ +import { useLingui } from "@lingui/react/macro"; import { Button, Input, Loader } from "@cloudflare/kumo"; import { Upload, Image, SquaresFour, List, MagnifyingGlass, Check, X } from "@phosphor-icons/react"; import { useQuery } from "@tanstack/react-query"; @@ -34,6 +35,7 @@ export function MediaLibrary({ onDelete, onItemUpdated, }: MediaLibraryProps) { + const { i18n, t } = useLingui(); const [viewMode, setViewMode] = React.useState<"grid" | "list">("grid"); const [selectedItem, setSelectedItem] = React.useState(null); const [activeProvider, setActiveProvider] = React.useState("local"); @@ -196,7 +198,7 @@ export function MediaLibrary({ // Build provider tabs const providerTabs = React.useMemo(() => { const tabs: Array<{ id: string; name: string; icon?: string }> = [ - { id: "local", name: "Library", icon: undefined }, + { id: "local", name: t`Library`, icon: undefined }, ]; if (providers) { for (const p of providers) { @@ -206,7 +208,7 @@ export function MediaLibrary({ } } return tabs; - }, [providers]); + }, [providers, i18n.locale, t]); // Get current items based on active provider const currentItems = activeProvider === "local" ? items : []; diff --git a/packages/admin/src/components/MediaPickerModal.tsx b/packages/admin/src/components/MediaPickerModal.tsx index 627c33f3b..ffc6d3d61 100644 --- a/packages/admin/src/components/MediaPickerModal.tsx +++ b/packages/admin/src/components/MediaPickerModal.tsx @@ -6,6 +6,7 @@ * Used by the rich text editor and image field components. */ +import { useLingui } from "@lingui/react/macro"; import { Button, Dialog, Input, Label, Loader } from "@cloudflare/kumo"; import { Upload, Image, Check, Globe, MagnifyingGlass } from "@phosphor-icons/react"; import { X } from "@phosphor-icons/react"; @@ -65,6 +66,7 @@ export function MediaPickerModal({ mimeTypeFilter = "image/", title = "Select Image", }: MediaPickerModalProps) { + const { i18n, t } = useLingui(); const queryClient = useQueryClient(); const [selectedItem, setSelectedItem] = React.useState(null); const [activeProvider, setActiveProvider] = React.useState("local"); @@ -322,7 +324,7 @@ export function MediaPickerModal({ // Filter out "local" from API response since we add it manually const providerTabs = React.useMemo(() => { const tabs: Array<{ id: string; name: string; icon?: string }> = [ - { id: "local", name: "Library", icon: undefined }, + { id: "local", name: t`Library`, icon: undefined }, ]; if (providers) { for (const p of providers) { @@ -332,7 +334,7 @@ export function MediaPickerModal({ } } return tabs; - }, [providers]); + }, [providers, i18n.locale, t]); return ( diff --git a/packages/admin/src/components/PortableTextEditor.tsx b/packages/admin/src/components/PortableTextEditor.tsx index d1e0f55b6..565cb101b 100644 --- a/packages/admin/src/components/PortableTextEditor.tsx +++ b/packages/admin/src/components/PortableTextEditor.tsx @@ -11,6 +11,8 @@ * - Floating menu on empty lines */ +import { t } from "@lingui/core/macro"; +import { useLingui } from "@lingui/react/macro"; import { Button, Dialog, Input } from "@cloudflare/kumo"; import type { Element } from "@emdash-cms/blocks"; import { useFloating, offset, flip, shift, autoUpdate } from "@floating-ui/react"; @@ -692,8 +694,8 @@ interface SlashCommandItem { const defaultSlashCommands: SlashCommandItem[] = [ { id: "heading1", - title: "Heading 1", - description: "Large section heading", + title: t`Heading 1`, + description: t`Large section heading`, icon: TextHOne, aliases: ["h1", "title"], command: ({ editor, range }) => { @@ -702,8 +704,8 @@ const defaultSlashCommands: SlashCommandItem[] = [ }, { id: "heading2", - title: "Heading 2", - description: "Medium section heading", + title: t`Heading 2`, + description: t`Medium section heading`, icon: TextHTwo, aliases: ["h2", "subtitle"], command: ({ editor, range }) => { @@ -712,8 +714,8 @@ const defaultSlashCommands: SlashCommandItem[] = [ }, { id: "heading3", - title: "Heading 3", - description: "Small section heading", + title: t`Heading 3`, + description: t`Small section heading`, icon: TextHThree, aliases: ["h3"], command: ({ editor, range }) => { @@ -722,8 +724,8 @@ const defaultSlashCommands: SlashCommandItem[] = [ }, { id: "bulletList", - title: "Bullet List", - description: "Create a bullet list", + title: t`Bullet List`, + description: t`Create a bullet list`, icon: List, aliases: ["ul", "unordered"], command: ({ editor, range }) => { @@ -732,8 +734,8 @@ const defaultSlashCommands: SlashCommandItem[] = [ }, { id: "numberedList", - title: "Numbered List", - description: "Create a numbered list", + title: t`Numbered List`, + description: t`Create a numbered list`, icon: ListNumbers, aliases: ["ol", "ordered"], command: ({ editor, range }) => { @@ -742,8 +744,8 @@ const defaultSlashCommands: SlashCommandItem[] = [ }, { id: "quote", - title: "Quote", - description: "Insert a blockquote", + title: t`Quote`, + description: t`Insert a blockquote`, icon: Quotes, aliases: ["blockquote", "cite"], command: ({ editor, range }) => { @@ -752,8 +754,8 @@ const defaultSlashCommands: SlashCommandItem[] = [ }, { id: "codeBlock", - title: "Code Block", - description: "Insert a code block", + title: t`Code Block`, + description: t`Insert a code block`, icon: CodeBlock, aliases: ["code", "pre", "```"], command: ({ editor, range }) => { @@ -762,8 +764,8 @@ const defaultSlashCommands: SlashCommandItem[] = [ }, { id: "divider", - title: "Divider", - description: "Insert a horizontal rule", + title: t`Divider`, + description: t`Insert a horizontal rule`, icon: Minus, aliases: ["hr", "---", "separator"], command: ({ editor, range }) => { @@ -1346,6 +1348,7 @@ export function PortableTextEditor({ onBlockSidebarOpen, onBlockSidebarClose, }: PortableTextEditorProps) { + const { i18n } = useLingui(); // Use a ref for onChange to avoid recreating the editor when the callback changes const onChangeRef = React.useRef(onChange); React.useEffect(() => { @@ -1399,11 +1402,11 @@ export function PortableTextEditor({ // Add image command cmds.push({ id: "image", - title: "Image", - description: "Insert an image", + title: t`Image`, + description: t`Insert an image`, icon: ImageIcon, aliases: ["img", "photo", "picture", "url"], - category: "Media", + category: t`Media`, command: ({ editor, range }) => { editor.chain().focus().deleteRange(range).run(); setMediaPickerOpen(true); @@ -1413,11 +1416,11 @@ export function PortableTextEditor({ // Add section command cmds.push({ id: "section", - title: "Section", - description: "Insert a reusable section", + title: t`Section`, + description: t`Insert a reusable section`, icon: Stack, aliases: ["pattern", "block", "template"], - category: "Content", + category: t`Content`, command: ({ editor, range }) => { editor.chain().focus().deleteRange(range).run(); setSectionPickerOpen(true); @@ -1429,10 +1432,10 @@ export function PortableTextEditor({ cmds.push({ id: `plugin-${block.pluginId}-${block.type}`, title: block.label, - description: block.description || `Embed a ${block.label.toLowerCase()}`, + description: block.description || t`Embed a ${block.label.toLowerCase()}`, icon: resolveIcon(block.icon), aliases: [block.type], - category: "Embeds", + category: t`Embeds`, command: ({ editor, range }) => { editor.chain().focus().deleteRange(range).run(); setPluginBlockModal(block); @@ -1441,7 +1444,7 @@ export function PortableTextEditor({ } return cmds; - }, [pluginBlocks]); + }, [pluginBlocks, i18n.locale]); // Filter commands by query — accessed via ref so the Suggestion plugin // (created once) always sees the latest command list without needing diff --git a/packages/admin/src/components/WelcomeModal.tsx b/packages/admin/src/components/WelcomeModal.tsx index 85b62ce4f..420642db0 100644 --- a/packages/admin/src/components/WelcomeModal.tsx +++ b/packages/admin/src/components/WelcomeModal.tsx @@ -4,6 +4,7 @@ * Shown to new users on their first login to welcome them to EmDash. */ +import { t } from "@lingui/core/macro"; import { Button, Dialog } from "@cloudflare/kumo"; import { X } from "@phosphor-icons/react"; import { useMutation, useQueryClient } from "@tanstack/react-query"; @@ -21,11 +22,11 @@ interface WelcomeModalProps { // 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"; + if (role >= 50) return t`Administrator`; + if (role >= 40) return t`Editor`; + if (role >= 30) return t`Author`; + if (role >= 20) return t`Contributor`; + return t`Subscriber`; } async function dismissWelcome(): Promise { diff --git a/packages/admin/src/components/Widgets.tsx b/packages/admin/src/components/Widgets.tsx index 2ed6a579b..be084f1b2 100644 --- a/packages/admin/src/components/Widgets.tsx +++ b/packages/admin/src/components/Widgets.tsx @@ -6,6 +6,7 @@ * Widgets within an area can be reordered via drag-and-drop. */ +import { t } from "@lingui/core/macro"; import { Button, Dialog, Input, Label, Select, Switch, Toast } from "@cloudflare/kumo"; import { DndContext, @@ -82,15 +83,15 @@ const BUILTIN_WIDGETS: Array<{ }> = [ { id: "palette-content", - label: "Content Block", - description: "Rich text content", - input: { type: "content", title: "Content Block" }, + label: t`Content Block`, + description: t`Rich text content`, + input: { type: "content", title: t`Content Block` }, }, { id: "palette-menu", - label: "Menu", - description: "Display a navigation menu", - input: { type: "menu", title: "Menu" }, + label: t`Menu`, + description: t`Display a navigation menu`, + input: { type: "menu", title: t`Menu` }, }, ]; diff --git a/packages/admin/src/components/editor/BlockMenu.tsx b/packages/admin/src/components/editor/BlockMenu.tsx index a0e75623e..b039f42d0 100644 --- a/packages/admin/src/components/editor/BlockMenu.tsx +++ b/packages/admin/src/components/editor/BlockMenu.tsx @@ -10,6 +10,7 @@ * Uses Floating UI for positioning relative to the selected block. */ +import { t } from "@lingui/core/macro"; import { Button } from "@cloudflare/kumo"; import { useFloating, offset, flip, shift, autoUpdate } from "@floating-ui/react"; import { @@ -47,7 +48,7 @@ interface BlockTransform { const blockTransforms: BlockTransform[] = [ { id: "paragraph", - label: "Paragraph", + label: t`Paragraph`, icon: Paragraph, transform: (editor) => { editor.chain().focus().setNode("paragraph").run(); @@ -55,7 +56,7 @@ const blockTransforms: BlockTransform[] = [ }, { id: "heading1", - label: "Heading 1", + label: t`Heading 1`, icon: TextHOne, transform: (editor) => { editor.chain().focus().setNode("heading", { level: 1 }).run(); @@ -63,7 +64,7 @@ const blockTransforms: BlockTransform[] = [ }, { id: "heading2", - label: "Heading 2", + label: t`Heading 2`, icon: TextHTwo, transform: (editor) => { editor.chain().focus().setNode("heading", { level: 2 }).run(); @@ -71,7 +72,7 @@ const blockTransforms: BlockTransform[] = [ }, { id: "heading3", - label: "Heading 3", + label: t`Heading 3`, icon: TextHThree, transform: (editor) => { editor.chain().focus().setNode("heading", { level: 3 }).run(); @@ -79,7 +80,7 @@ const blockTransforms: BlockTransform[] = [ }, { id: "blockquote", - label: "Quote", + label: t`Quote`, icon: Quotes, transform: (editor) => { editor.chain().focus().toggleBlockquote().run(); @@ -87,7 +88,7 @@ const blockTransforms: BlockTransform[] = [ }, { id: "codeBlock", - label: "Code Block", + label: t`Code Block`, icon: Code, transform: (editor) => { editor.chain().focus().toggleCodeBlock().run(); @@ -95,7 +96,7 @@ const blockTransforms: BlockTransform[] = [ }, { id: "bulletList", - label: "Bullet List", + label: t`Bullet List`, icon: List, transform: (editor) => { editor.chain().focus().toggleBulletList().run(); @@ -103,7 +104,7 @@ const blockTransforms: BlockTransform[] = [ }, { id: "orderedList", - label: "Numbered List", + label: t`Numbered List`, icon: ListNumbers, transform: (editor) => { editor.chain().focus().toggleOrderedList().run(); diff --git a/packages/admin/src/components/settings/AllowedDomainsSettings.tsx b/packages/admin/src/components/settings/AllowedDomainsSettings.tsx index 22bc6e2a9..2b1fb6cbb 100644 --- a/packages/admin/src/components/settings/AllowedDomainsSettings.tsx +++ b/packages/admin/src/components/settings/AllowedDomainsSettings.tsx @@ -5,6 +5,7 @@ * is configured, this page shows an informational message instead. */ +import { t } from "@lingui/core/macro"; import { Button, Dialog, Input, Select, Switch } from "@cloudflare/kumo"; import { Globe, @@ -31,14 +32,14 @@ import { } from "../../lib/api"; const ROLES = [ - { value: 10, label: "Subscriber" }, - { value: 20, label: "Contributor" }, - { value: 30, label: "Author" }, - { value: 40, label: "Editor" }, + { value: 10, label: t`Subscriber` }, + { value: 20, label: t`Contributor` }, + { value: 30, label: t`Author` }, + { value: 40, label: t`Editor` }, ] as const; function getRoleName(level: number): string { - return ROLES.find((r) => r.value === level)?.label ?? "Unknown"; + return ROLES.find((r) => r.value === level)?.label ?? t`Unknown`; } export function AllowedDomainsSettings() { diff --git a/packages/admin/src/components/settings/ApiTokenSettings.tsx b/packages/admin/src/components/settings/ApiTokenSettings.tsx index 86ba4fbd0..30779ccca 100644 --- a/packages/admin/src/components/settings/ApiTokenSettings.tsx +++ b/packages/admin/src/components/settings/ApiTokenSettings.tsx @@ -4,6 +4,7 @@ * Allows admins to list, create, and revoke Personal Access Tokens. */ +import { t } from "@lingui/core/macro"; import { Button, Checkbox, Input, Loader, Select } from "@cloudflare/kumo"; import { ArrowLeft, @@ -33,11 +34,11 @@ 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: t`No expiry` }, + { value: "7d", label: t`7 days` }, + { value: "30d", label: t`30 days` }, + { value: "90d", label: t`90 days` }, + { value: "365d", label: t`1 year` }, ] as const; function computeExpiryDate(option: string): string | undefined { diff --git a/packages/admin/src/components/users/RoleBadge.tsx b/packages/admin/src/components/users/RoleBadge.tsx index 49be48a08..6a45312e4 100644 --- a/packages/admin/src/components/users/RoleBadge.tsx +++ b/packages/admin/src/components/users/RoleBadge.tsx @@ -1,31 +1,32 @@ +import { t } from "@lingui/core/macro"; import { cn } from "../../lib/utils"; /** Role level to name mapping */ const ROLE_CONFIG: Record = { 10: { - label: "Subscriber", + label: t`Subscriber`, color: "gray", - description: "Can view content", + description: t`Can view content`, }, 20: { - label: "Contributor", + label: t`Contributor`, color: "blue", - description: "Can create content", + description: t`Can create content`, }, 30: { - label: "Author", + label: t`Author`, color: "green", - description: "Can publish own content", + description: t`Can publish own content`, }, 40: { - label: "Editor", + label: t`Editor`, color: "purple", - description: "Can manage all content", + description: t`Can manage all content`, }, 50: { - label: "Admin", + label: t`Admin`, color: "red", - description: "Full access", + description: t`Full access`, }, }; @@ -33,9 +34,9 @@ const ROLE_CONFIG: Record Date: Sun, 12 Apr 2026 01:48:39 +0300 Subject: [PATCH 03/48] Extract module-level strings to locale catalogs Runs lingui extract to discover and catalog all newly wrapped t\`\` strings from the previous commit. Adds 130+ new message IDs to both English and German catalogs. English catalog includes auto-filled translations (msgid = msgstr for en). German catalog entries are marked for translation (empty msgstr). Generated by: pnpm locale:extract --- packages/admin/src/locales/de/messages.po | 397 ++++++++++++++++++++++ packages/admin/src/locales/en/messages.po | 397 ++++++++++++++++++++++ 2 files changed, 794 insertions(+) diff --git a/packages/admin/src/locales/de/messages.po b/packages/admin/src/locales/de/messages.po index f94b1bc24..3f40dea5f 100644 --- a/packages/admin/src/locales/de/messages.po +++ b/packages/admin/src/locales/de/messages.po @@ -25,6 +25,31 @@ msgstr "{label} — keine Übersetzung" msgid "{label} — view translation" msgstr "{label} — Übersetzung anzeigen" +#: packages/admin/src/components/settings/ApiTokenSettings.tsx:41 +msgid "1 year" +msgstr "" + +#: packages/admin/src/components/settings/ApiTokenSettings.tsx:39 +msgid "30 days" +msgstr "" + +#: packages/admin/src/components/settings/ApiTokenSettings.tsx:38 +msgid "7 days" +msgstr "" + +#: packages/admin/src/components/settings/ApiTokenSettings.tsx:40 +msgid "90 days" +msgstr "" + +#: packages/admin/src/components/users/RoleBadge.tsx:27 +#: packages/admin/src/lib/api/api-tokens.ts:47 +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" @@ -41,11 +66,42 @@ msgstr "API-Tokens" msgid "Authentication error: {error}" msgstr "Authentifizierungsfehler: {error}" +#: packages/admin/src/components/settings/AllowedDomainsSettings.tsx:37 +#: packages/admin/src/components/users/RoleBadge.tsx:17 +#: 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/editor/BlockMenu.tsx:99 +#: packages/admin/src/components/PortableTextEditor.tsx:727 +msgid "Bullet List" +msgstr "" + +#: packages/admin/src/components/users/RoleBadge.tsx:14 +msgid "Can create content" +msgstr "" + +#: packages/admin/src/components/users/RoleBadge.tsx:24 +msgid "Can manage all content" +msgstr "" + +#: packages/admin/src/components/users/RoleBadge.tsx:19 +msgid "Can publish own content" +msgstr "" + +#: packages/admin/src/components/users/RoleBadge.tsx:9 +msgid "Can view content" +msgstr "" + +#: packages/admin/src/components/AdminCommandPalette.tsx:201 +msgid "Categories" +msgstr "" + #: packages/admin/src/components/LoginPage.tsx:158 msgid "Check your email" msgstr "Überprüfen Sie Ihre E-Mail" @@ -58,14 +114,88 @@ 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/editor/BlockMenu.tsx:91 +#: packages/admin/src/components/PortableTextEditor.tsx:757 +msgid "Code Block" +msgstr "" + +#: packages/admin/src/components/PortableTextEditor.tsx:1423 +msgid "Content" +msgstr "" + +#: packages/admin/src/components/Widgets.tsx:86 +#: packages/admin/src/components/Widgets.tsx:88 +msgid "Content Block" +msgstr "" + +#: packages/admin/src/lib/api/api-tokens.ts:41 +msgid "Content Read" +msgstr "" + +#: packages/admin/src/components/AdminCommandPalette.tsx:193 +msgid "Content Types" +msgstr "" + +#: packages/admin/src/lib/api/api-tokens.ts:42 +msgid "Content Write" +msgstr "" + +#: packages/admin/src/components/settings/AllowedDomainsSettings.tsx:36 +#: packages/admin/src/components/users/RoleBadge.tsx:12 +#: packages/admin/src/components/WelcomeModal.tsx:28 +msgid "Contributor" +msgstr "" + +#: packages/admin/src/components/PortableTextEditor.tsx:728 +msgid "Create a bullet list" +msgstr "" + +#: packages/admin/src/components/PortableTextEditor.tsx:738 +msgid "Create a numbered list" +msgstr "" + #: packages/admin/src/components/Settings.tsx:110 msgid "Create personal access tokens for programmatic API access" msgstr "Persönliche Zugangstokens für programmatischen API-Zugriff erstellen" +#: packages/admin/src/lib/api/api-tokens.ts:42 +msgid "Create, update, delete content" +msgstr "" + +#: packages/admin/src/components/ContentTypeEditor.tsx:106 +msgid "Created At" +msgstr "" + +#: packages/admin/src/components/AdminCommandPalette.tsx:139 +msgid "Dashboard" +msgstr "" + +#: packages/admin/src/components/Widgets.tsx:93 +msgid "Display a navigation menu" +msgstr "" + +#: packages/admin/src/components/PortableTextEditor.tsx:767 +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:102 +msgid "draft, published, or archived" +msgstr "" + +#: packages/admin/src/components/ContentTypeEditor.tsx:61 +msgid "Drafts" +msgstr "" + +#: packages/admin/src/components/settings/AllowedDomainsSettings.tsx:38 +#: packages/admin/src/components/users/RoleBadge.tsx:22 +#: packages/admin/src/components/WelcomeModal.tsx:26 +msgid "Editor" +msgstr "" + #: packages/admin/src/components/Settings.tsx:115 msgid "Email" msgstr "E-Mail" @@ -74,23 +204,100 @@ msgstr "E-Mail" msgid "Email address" msgstr "E-Mail-Adresse" +#. placeholder {0}: block.label.toLowerCase() +#: packages/admin/src/components/PortableTextEditor.tsx:1435 +msgid "Embed a {0}" +msgstr "" + +#: packages/admin/src/components/PortableTextEditor.tsx:1438 +msgid "Embeds" +msgstr "" + +#: packages/admin/src/components/ContentTypeEditor.tsx:77 +msgid "Enable full-text search on this collection" +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/RoleBadge.tsx:29 +msgid "Full access" +msgstr "" + +#: packages/admin/src/lib/api/api-tokens.ts:47 +msgid "Full admin access" +msgstr "" + #: packages/admin/src/components/Settings.tsx:69 msgid "General" msgstr "Allgemein" +#: packages/admin/src/components/editor/BlockMenu.tsx:59 +#: packages/admin/src/components/PortableTextEditor.tsx:697 +msgid "Heading 1" +msgstr "" + +#: packages/admin/src/components/editor/BlockMenu.tsx:67 +#: packages/admin/src/components/PortableTextEditor.tsx:707 +msgid "Heading 2" +msgstr "" + +#: packages/admin/src/components/editor/BlockMenu.tsx:75 +#: packages/admin/src/components/PortableTextEditor.tsx:717 +msgid "Heading 3" +msgstr "" + +#: packages/admin/src/components/ContentTypeEditor.tsx:88 +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:1405 +msgid "Image" +msgstr "" + +#: packages/admin/src/components/AdminCommandPalette.tsx:235 +msgid "Import" +msgstr "" + +#: packages/admin/src/components/PortableTextEditor.tsx:748 +msgid "Insert a blockquote" +msgstr "" + +#: packages/admin/src/components/PortableTextEditor.tsx:758 +msgid "Insert a code block" +msgstr "" + +#: packages/admin/src/components/PortableTextEditor.tsx:768 +msgid "Insert a horizontal rule" +msgstr "" + +#: packages/admin/src/components/PortableTextEditor.tsx:1420 +msgid "Insert a reusable section" +msgstr "" + +#: packages/admin/src/components/PortableTextEditor.tsx:1406 +msgid "Insert an image" +msgstr "" + #: packages/admin/src/components/Settings.tsx:129 msgid "Language" msgstr "Sprache" +#: packages/admin/src/components/PortableTextEditor.tsx:698 +msgid "Large section heading" +msgstr "" + +#: packages/admin/src/components/MediaLibrary.tsx:201 +#: packages/admin/src/components/MediaPickerModal.tsx:327 +msgid "Library" +msgstr "" + #: packages/admin/src/components/LocaleSwitcher.tsx:60 msgid "Locale" msgstr "Sprache" @@ -99,18 +306,137 @@ msgstr "Sprache" msgid "Manage your passkeys and authentication" msgstr "Passkeys und Authentifizierung verwalten" +#: packages/admin/src/components/PortableTextEditor.tsx:1409 +msgid "Media" +msgstr "" + +#: packages/admin/src/components/AdminCommandPalette.tsx:162 +msgid "Media Library" +msgstr "" + +#: packages/admin/src/lib/api/api-tokens.ts:43 +msgid "Media Read" +msgstr "" + +#: packages/admin/src/lib/api/api-tokens.ts:44 +msgid "Media Write" +msgstr "" + +#: packages/admin/src/components/PortableTextEditor.tsx:708 +msgid "Medium section heading" +msgstr "" + +#: packages/admin/src/components/Widgets.tsx:92 +#: packages/admin/src/components/Widgets.tsx:94 +msgid "Menu" +msgstr "" + +#: packages/admin/src/components/AdminCommandPalette.tsx:169 +msgid "Menus" +msgstr "" + +#: packages/admin/src/lib/api/api-tokens.ts:46 +msgid "Modify collection schemas" +msgstr "" + +#: packages/admin/src/components/settings/ApiTokenSettings.tsx:37 +msgid "No expiry" +msgstr "" + +#: packages/admin/src/components/editor/BlockMenu.tsx:107 +#: packages/admin/src/components/PortableTextEditor.tsx:737 +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:51 +msgid "Paragraph" +msgstr "" + +#: packages/admin/src/components/AdminCommandPalette.tsx:227 +msgid "Plugins" +msgstr "" + +#: packages/admin/src/components/ContentTypeEditor.tsx:71 +msgid "Preview" +msgstr "" + +#: packages/admin/src/components/ContentTypeEditor.tsx:72 +msgid "Preview content before publishing" +msgstr "" + +#: packages/admin/src/components/ContentTypeEditor.tsx:118 +msgid "Published At" +msgstr "" + +#: packages/admin/src/components/editor/BlockMenu.tsx:83 +#: packages/admin/src/components/PortableTextEditor.tsx:747 +msgid "Quote" +msgstr "" + +#: packages/admin/src/lib/api/api-tokens.ts:45 +msgid "Read collection schemas" +msgstr "" + +#: packages/admin/src/lib/api/api-tokens.ts:41 +msgid "Read content entries" +msgstr "" + +#: packages/admin/src/lib/api/api-tokens.ts:43 +msgid "Read media files" +msgstr "" + +#: packages/admin/src/components/ContentTypeEditor.tsx:66 +msgid "Revisions" +msgstr "" + +#: packages/admin/src/components/Widgets.tsx:87 +msgid "Rich text content" +msgstr "" + +#: packages/admin/src/components/users/RoleBadge.tsx:37 +msgid "Role {role}" +msgstr "" + +#: packages/admin/src/components/ContentTypeEditor.tsx:62 +msgid "Save content as draft before publishing" +msgstr "" + +#: packages/admin/src/lib/api/api-tokens.ts:45 +msgid "Schema Read" +msgstr "" + +#: packages/admin/src/lib/api/api-tokens.ts:46 +msgid "Schema Write" +msgstr "" + +#: packages/admin/src/components/ContentTypeEditor.tsx:76 +msgid "Search" +msgstr "" + #: packages/admin/src/components/Settings.tsx:82 msgid "Search engine optimization and verification" msgstr "Suchmaschinenoptimierung und Verifizierung" +#: packages/admin/src/components/PortableTextEditor.tsx:1419 +msgid "Section" +msgstr "" + +#: packages/admin/src/components/AdminCommandPalette.tsx:185 +msgid "Sections" +msgstr "" + #: packages/admin/src/components/Settings.tsx:92 msgid "Security" msgstr "Sicherheit" +#: packages/admin/src/components/AdminCommandPalette.tsx:251 +msgid "Security Settings" +msgstr "" + #: packages/admin/src/components/Settings.tsx:98 msgid "Self-Signup Domains" msgstr "Selbstregistrierungs-Domains" @@ -127,6 +453,7 @@ msgstr "Wird gesendet..." msgid "SEO" msgstr "SEO" +#: packages/admin/src/components/AdminCommandPalette.tsx:243 #: packages/admin/src/components/Settings.tsx:62 msgid "Settings" msgstr "Einstellungen" @@ -151,6 +478,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:94 +msgid "Slug" +msgstr "" + +#: packages/admin/src/components/PortableTextEditor.tsx:718 +msgid "Small section heading" +msgstr "" + #: packages/admin/src/components/Settings.tsx:75 msgid "Social Links" msgstr "Soziale Netzwerke" @@ -159,14 +494,60 @@ msgstr "Soziale Netzwerke" msgid "Social media profile links" msgstr "Links zu Social-Media-Profilen" +#: packages/admin/src/components/ContentTypeEditor.tsx:100 +msgid "Status" +msgstr "" + +#: packages/admin/src/components/settings/AllowedDomainsSettings.tsx:35 +#: packages/admin/src/components/users/RoleBadge.tsx:7 +#: packages/admin/src/components/WelcomeModal.tsx:29 +msgid "Subscriber" +msgstr "" + +#: packages/admin/src/components/AdminCommandPalette.tsx:210 +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/ContentTypeEditor.tsx:67 +msgid "Track content history" +msgstr "" + +#: packages/admin/src/components/ContentTypeEditor.tsx:90 +msgid "Unique identifier (ULID)" +msgstr "" + +#: packages/admin/src/components/settings/AllowedDomainsSettings.tsx:42 +msgid "Unknown" +msgstr "" + +#: packages/admin/src/components/users/RoleBadge.tsx:39 +msgid "Unknown role" +msgstr "" + +#: packages/admin/src/components/ContentTypeEditor.tsx:112 +msgid "Updated At" +msgstr "" + +#: packages/admin/src/lib/api/api-tokens.ts:44 +msgid "Upload and delete media" +msgstr "" + +#: packages/admin/src/components/ContentTypeEditor.tsx:96 +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:219 +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 +555,19 @@ 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/ContentTypeEditor.tsx:108 +msgid "When the entry was created" +msgstr "" + +#: packages/admin/src/components/ContentTypeEditor.tsx:114 +msgid "When the entry was last modified" +msgstr "" + +#: packages/admin/src/components/ContentTypeEditor.tsx:120 +msgid "When the entry was published" +msgstr "" + +#: packages/admin/src/components/AdminCommandPalette.tsx:177 +msgid "Widgets" +msgstr "" diff --git a/packages/admin/src/locales/en/messages.po b/packages/admin/src/locales/en/messages.po index fd4296104..6306a75c5 100644 --- a/packages/admin/src/locales/en/messages.po +++ b/packages/admin/src/locales/en/messages.po @@ -25,6 +25,31 @@ msgstr "{label} — no translation" msgid "{label} — view translation" msgstr "{label} — view translation" +#: packages/admin/src/components/settings/ApiTokenSettings.tsx:41 +msgid "1 year" +msgstr "1 year" + +#: packages/admin/src/components/settings/ApiTokenSettings.tsx:39 +msgid "30 days" +msgstr "30 days" + +#: packages/admin/src/components/settings/ApiTokenSettings.tsx:38 +msgid "7 days" +msgstr "7 days" + +#: packages/admin/src/components/settings/ApiTokenSettings.tsx:40 +msgid "90 days" +msgstr "90 days" + +#: packages/admin/src/components/users/RoleBadge.tsx:27 +#: packages/admin/src/lib/api/api-tokens.ts:47 +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" @@ -41,11 +66,42 @@ msgstr "API Tokens" msgid "Authentication error: {error}" msgstr "Authentication error: {error}" +#: packages/admin/src/components/settings/AllowedDomainsSettings.tsx:37 +#: packages/admin/src/components/users/RoleBadge.tsx:17 +#: 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/editor/BlockMenu.tsx:99 +#: packages/admin/src/components/PortableTextEditor.tsx:727 +msgid "Bullet List" +msgstr "Bullet List" + +#: packages/admin/src/components/users/RoleBadge.tsx:14 +msgid "Can create content" +msgstr "Can create content" + +#: packages/admin/src/components/users/RoleBadge.tsx:24 +msgid "Can manage all content" +msgstr "Can manage all content" + +#: packages/admin/src/components/users/RoleBadge.tsx:19 +msgid "Can publish own content" +msgstr "Can publish own content" + +#: packages/admin/src/components/users/RoleBadge.tsx:9 +msgid "Can view content" +msgstr "Can view content" + +#: packages/admin/src/components/AdminCommandPalette.tsx:201 +msgid "Categories" +msgstr "Categories" + #: packages/admin/src/components/LoginPage.tsx:158 msgid "Check your email" msgstr "Check your email" @@ -58,14 +114,88 @@ 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/editor/BlockMenu.tsx:91 +#: packages/admin/src/components/PortableTextEditor.tsx:757 +msgid "Code Block" +msgstr "Code Block" + +#: packages/admin/src/components/PortableTextEditor.tsx:1423 +msgid "Content" +msgstr "Content" + +#: packages/admin/src/components/Widgets.tsx:86 +#: packages/admin/src/components/Widgets.tsx:88 +msgid "Content Block" +msgstr "Content Block" + +#: packages/admin/src/lib/api/api-tokens.ts:41 +msgid "Content Read" +msgstr "Content Read" + +#: packages/admin/src/components/AdminCommandPalette.tsx:193 +msgid "Content Types" +msgstr "Content Types" + +#: packages/admin/src/lib/api/api-tokens.ts:42 +msgid "Content Write" +msgstr "Content Write" + +#: packages/admin/src/components/settings/AllowedDomainsSettings.tsx:36 +#: packages/admin/src/components/users/RoleBadge.tsx:12 +#: packages/admin/src/components/WelcomeModal.tsx:28 +msgid "Contributor" +msgstr "Contributor" + +#: packages/admin/src/components/PortableTextEditor.tsx:728 +msgid "Create a bullet list" +msgstr "Create a bullet list" + +#: packages/admin/src/components/PortableTextEditor.tsx:738 +msgid "Create a numbered list" +msgstr "Create a numbered list" + #: packages/admin/src/components/Settings.tsx:110 msgid "Create personal access tokens for programmatic API access" msgstr "Create personal access tokens for programmatic API access" +#: packages/admin/src/lib/api/api-tokens.ts:42 +msgid "Create, update, delete content" +msgstr "Create, update, delete content" + +#: packages/admin/src/components/ContentTypeEditor.tsx:106 +msgid "Created At" +msgstr "Created At" + +#: packages/admin/src/components/AdminCommandPalette.tsx:139 +msgid "Dashboard" +msgstr "Dashboard" + +#: packages/admin/src/components/Widgets.tsx:93 +msgid "Display a navigation menu" +msgstr "Display a navigation menu" + +#: packages/admin/src/components/PortableTextEditor.tsx:767 +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:102 +msgid "draft, published, or archived" +msgstr "draft, published, or archived" + +#: packages/admin/src/components/ContentTypeEditor.tsx:61 +msgid "Drafts" +msgstr "Drafts" + +#: packages/admin/src/components/settings/AllowedDomainsSettings.tsx:38 +#: packages/admin/src/components/users/RoleBadge.tsx:22 +#: packages/admin/src/components/WelcomeModal.tsx:26 +msgid "Editor" +msgstr "Editor" + #: packages/admin/src/components/Settings.tsx:115 msgid "Email" msgstr "Email" @@ -74,23 +204,100 @@ msgstr "Email" msgid "Email address" msgstr "Email address" +#. placeholder {0}: block.label.toLowerCase() +#: packages/admin/src/components/PortableTextEditor.tsx:1435 +msgid "Embed a {0}" +msgstr "Embed a {0}" + +#: packages/admin/src/components/PortableTextEditor.tsx:1438 +msgid "Embeds" +msgstr "Embeds" + +#: packages/admin/src/components/ContentTypeEditor.tsx:77 +msgid "Enable full-text search on this collection" +msgstr "Enable full-text search on this collection" + #: 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/RoleBadge.tsx:29 +msgid "Full access" +msgstr "Full access" + +#: packages/admin/src/lib/api/api-tokens.ts:47 +msgid "Full admin access" +msgstr "Full admin access" + #: packages/admin/src/components/Settings.tsx:69 msgid "General" msgstr "General" +#: packages/admin/src/components/editor/BlockMenu.tsx:59 +#: packages/admin/src/components/PortableTextEditor.tsx:697 +msgid "Heading 1" +msgstr "Heading 1" + +#: packages/admin/src/components/editor/BlockMenu.tsx:67 +#: packages/admin/src/components/PortableTextEditor.tsx:707 +msgid "Heading 2" +msgstr "Heading 2" + +#: packages/admin/src/components/editor/BlockMenu.tsx:75 +#: packages/admin/src/components/PortableTextEditor.tsx:717 +msgid "Heading 3" +msgstr "Heading 3" + +#: packages/admin/src/components/ContentTypeEditor.tsx:88 +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:1405 +msgid "Image" +msgstr "Image" + +#: packages/admin/src/components/AdminCommandPalette.tsx:235 +msgid "Import" +msgstr "Import" + +#: packages/admin/src/components/PortableTextEditor.tsx:748 +msgid "Insert a blockquote" +msgstr "Insert a blockquote" + +#: packages/admin/src/components/PortableTextEditor.tsx:758 +msgid "Insert a code block" +msgstr "Insert a code block" + +#: packages/admin/src/components/PortableTextEditor.tsx:768 +msgid "Insert a horizontal rule" +msgstr "Insert a horizontal rule" + +#: packages/admin/src/components/PortableTextEditor.tsx:1420 +msgid "Insert a reusable section" +msgstr "Insert a reusable section" + +#: packages/admin/src/components/PortableTextEditor.tsx:1406 +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:698 +msgid "Large section heading" +msgstr "Large section heading" + +#: packages/admin/src/components/MediaLibrary.tsx:201 +#: packages/admin/src/components/MediaPickerModal.tsx:327 +msgid "Library" +msgstr "Library" + #: packages/admin/src/components/LocaleSwitcher.tsx:60 msgid "Locale" msgstr "Locale" @@ -99,18 +306,137 @@ msgstr "Locale" msgid "Manage your passkeys and authentication" msgstr "Manage your passkeys and authentication" +#: packages/admin/src/components/PortableTextEditor.tsx:1409 +msgid "Media" +msgstr "Media" + +#: packages/admin/src/components/AdminCommandPalette.tsx:162 +msgid "Media Library" +msgstr "Media Library" + +#: packages/admin/src/lib/api/api-tokens.ts:43 +msgid "Media Read" +msgstr "Media Read" + +#: packages/admin/src/lib/api/api-tokens.ts:44 +msgid "Media Write" +msgstr "Media Write" + +#: packages/admin/src/components/PortableTextEditor.tsx:708 +msgid "Medium section heading" +msgstr "Medium section heading" + +#: packages/admin/src/components/Widgets.tsx:92 +#: packages/admin/src/components/Widgets.tsx:94 +msgid "Menu" +msgstr "Menu" + +#: packages/admin/src/components/AdminCommandPalette.tsx:169 +msgid "Menus" +msgstr "Menus" + +#: packages/admin/src/lib/api/api-tokens.ts:46 +msgid "Modify collection schemas" +msgstr "Modify collection schemas" + +#: packages/admin/src/components/settings/ApiTokenSettings.tsx:37 +msgid "No expiry" +msgstr "No expiry" + +#: packages/admin/src/components/editor/BlockMenu.tsx:107 +#: packages/admin/src/components/PortableTextEditor.tsx:737 +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:51 +msgid "Paragraph" +msgstr "Paragraph" + +#: packages/admin/src/components/AdminCommandPalette.tsx:227 +msgid "Plugins" +msgstr "Plugins" + +#: packages/admin/src/components/ContentTypeEditor.tsx:71 +msgid "Preview" +msgstr "Preview" + +#: packages/admin/src/components/ContentTypeEditor.tsx:72 +msgid "Preview content before publishing" +msgstr "Preview content before publishing" + +#: packages/admin/src/components/ContentTypeEditor.tsx:118 +msgid "Published At" +msgstr "Published At" + +#: packages/admin/src/components/editor/BlockMenu.tsx:83 +#: packages/admin/src/components/PortableTextEditor.tsx:747 +msgid "Quote" +msgstr "Quote" + +#: packages/admin/src/lib/api/api-tokens.ts:45 +msgid "Read collection schemas" +msgstr "Read collection schemas" + +#: packages/admin/src/lib/api/api-tokens.ts:41 +msgid "Read content entries" +msgstr "Read content entries" + +#: packages/admin/src/lib/api/api-tokens.ts:43 +msgid "Read media files" +msgstr "Read media files" + +#: packages/admin/src/components/ContentTypeEditor.tsx:66 +msgid "Revisions" +msgstr "Revisions" + +#: packages/admin/src/components/Widgets.tsx:87 +msgid "Rich text content" +msgstr "Rich text content" + +#: packages/admin/src/components/users/RoleBadge.tsx:37 +msgid "Role {role}" +msgstr "Role {role}" + +#: packages/admin/src/components/ContentTypeEditor.tsx:62 +msgid "Save content as draft before publishing" +msgstr "Save content as draft before publishing" + +#: packages/admin/src/lib/api/api-tokens.ts:45 +msgid "Schema Read" +msgstr "Schema Read" + +#: packages/admin/src/lib/api/api-tokens.ts:46 +msgid "Schema Write" +msgstr "Schema Write" + +#: packages/admin/src/components/ContentTypeEditor.tsx:76 +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/PortableTextEditor.tsx:1419 +msgid "Section" +msgstr "Section" + +#: packages/admin/src/components/AdminCommandPalette.tsx:185 +msgid "Sections" +msgstr "Sections" + #: packages/admin/src/components/Settings.tsx:92 msgid "Security" msgstr "Security" +#: packages/admin/src/components/AdminCommandPalette.tsx:251 +msgid "Security Settings" +msgstr "Security Settings" + #: packages/admin/src/components/Settings.tsx:98 msgid "Self-Signup Domains" msgstr "Self-Signup Domains" @@ -127,6 +453,7 @@ msgstr "Sending..." msgid "SEO" msgstr "SEO" +#: packages/admin/src/components/AdminCommandPalette.tsx:243 #: packages/admin/src/components/Settings.tsx:62 msgid "Settings" msgstr "Settings" @@ -151,6 +478,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:94 +msgid "Slug" +msgstr "Slug" + +#: packages/admin/src/components/PortableTextEditor.tsx:718 +msgid "Small section heading" +msgstr "Small section heading" + #: packages/admin/src/components/Settings.tsx:75 msgid "Social Links" msgstr "Social Links" @@ -159,14 +494,60 @@ msgstr "Social Links" msgid "Social media profile links" msgstr "Social media profile links" +#: packages/admin/src/components/ContentTypeEditor.tsx:100 +msgid "Status" +msgstr "Status" + +#: packages/admin/src/components/settings/AllowedDomainsSettings.tsx:35 +#: packages/admin/src/components/users/RoleBadge.tsx:7 +#: packages/admin/src/components/WelcomeModal.tsx:29 +msgid "Subscriber" +msgstr "Subscriber" + +#: packages/admin/src/components/AdminCommandPalette.tsx:210 +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/ContentTypeEditor.tsx:67 +msgid "Track content history" +msgstr "Track content history" + +#: packages/admin/src/components/ContentTypeEditor.tsx:90 +msgid "Unique identifier (ULID)" +msgstr "Unique identifier (ULID)" + +#: packages/admin/src/components/settings/AllowedDomainsSettings.tsx:42 +msgid "Unknown" +msgstr "Unknown" + +#: packages/admin/src/components/users/RoleBadge.tsx:39 +msgid "Unknown role" +msgstr "Unknown role" + +#: packages/admin/src/components/ContentTypeEditor.tsx:112 +msgid "Updated At" +msgstr "Updated At" + +#: packages/admin/src/lib/api/api-tokens.ts:44 +msgid "Upload and delete media" +msgstr "Upload and delete media" + +#: packages/admin/src/components/ContentTypeEditor.tsx:96 +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:219 +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 +555,19 @@ 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/ContentTypeEditor.tsx:108 +msgid "When the entry was created" +msgstr "When the entry was created" + +#: packages/admin/src/components/ContentTypeEditor.tsx:114 +msgid "When the entry was last modified" +msgstr "When the entry was last modified" + +#: packages/admin/src/components/ContentTypeEditor.tsx:120 +msgid "When the entry was published" +msgstr "When the entry was published" + +#: packages/admin/src/components/AdminCommandPalette.tsx:177 +msgid "Widgets" +msgstr "Widgets" From fb4ec9dd1f80e93bafebb49ab2b1e0622e87d083 Mon Sep 17 00:00:00 2001 From: Ophir Bucai Date: Sun, 12 Apr 2026 01:48:46 +0300 Subject: [PATCH 04/48] Add TypeScript declarations for compiled message catalogs --- packages/admin/src/locales/en/messages.mjs.d.ts | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 packages/admin/src/locales/en/messages.mjs.d.ts diff --git a/packages/admin/src/locales/en/messages.mjs.d.ts b/packages/admin/src/locales/en/messages.mjs.d.ts new file mode 100644 index 000000000..1b8c01899 --- /dev/null +++ b/packages/admin/src/locales/en/messages.mjs.d.ts @@ -0,0 +1,5 @@ +declare module "*/messages.mjs" { + import type { Messages } from "@lingui/core"; + + export const messages: Messages; +} From a5890c14702ee2c3f57176b73726c8269fa372ed Mon Sep 17 00:00:00 2001 From: Ophir Bucai Date: Sun, 12 Apr 2026 01:55:49 +0300 Subject: [PATCH 05/48] Format code with oxfmt and prettier --- packages/admin/src/components/AdminCommandPalette.tsx | 2 +- packages/admin/src/components/ContentTypeEditor.tsx | 2 +- packages/admin/src/components/MediaLibrary.tsx | 2 +- packages/admin/src/components/MediaPickerModal.tsx | 2 +- packages/admin/src/components/PortableTextEditor.tsx | 4 ++-- packages/admin/src/components/WelcomeModal.tsx | 2 +- packages/admin/src/components/Widgets.tsx | 2 +- packages/admin/src/components/editor/BlockMenu.tsx | 2 +- .../src/components/settings/AllowedDomainsSettings.tsx | 2 +- .../admin/src/components/settings/ApiTokenSettings.tsx | 2 +- packages/admin/src/components/users/RoleBadge.tsx | 1 + packages/admin/src/lib/api/api-tokens.ts | 7 ++++++- packages/admin/src/locales/en/messages.mjs.d.ts | 2 +- packages/admin/src/locales/init.ts | 5 +++-- 14 files changed, 22 insertions(+), 15 deletions(-) diff --git a/packages/admin/src/components/AdminCommandPalette.tsx b/packages/admin/src/components/AdminCommandPalette.tsx index 203b096da..678743f92 100644 --- a/packages/admin/src/components/AdminCommandPalette.tsx +++ b/packages/admin/src/components/AdminCommandPalette.tsx @@ -5,9 +5,9 @@ * Opens with Cmd+K (Mac) or Ctrl+K (Windows/Linux). */ +import { CommandPalette } from "@cloudflare/kumo"; import { t } from "@lingui/core/macro"; import { useLingui } from "@lingui/react/macro"; -import { CommandPalette } from "@cloudflare/kumo"; import { SquaresFour, FileText, diff --git a/packages/admin/src/components/ContentTypeEditor.tsx b/packages/admin/src/components/ContentTypeEditor.tsx index 1ede1b98d..928c68ce0 100644 --- a/packages/admin/src/components/ContentTypeEditor.tsx +++ b/packages/admin/src/components/ContentTypeEditor.tsx @@ -1,4 +1,3 @@ -import { t } from "@lingui/core/macro"; import { Badge, Button, Input, InputArea, Label, Select, buttonVariants } from "@cloudflare/kumo"; import { DndContext, @@ -17,6 +16,7 @@ import { verticalListSortingStrategy, } from "@dnd-kit/sortable"; import { CSS } from "@dnd-kit/utilities"; +import { t } from "@lingui/core/macro"; import { ArrowLeft, Plus, diff --git a/packages/admin/src/components/MediaLibrary.tsx b/packages/admin/src/components/MediaLibrary.tsx index 92d490aa7..f821b1679 100644 --- a/packages/admin/src/components/MediaLibrary.tsx +++ b/packages/admin/src/components/MediaLibrary.tsx @@ -1,5 +1,5 @@ -import { useLingui } from "@lingui/react/macro"; import { Button, Input, Loader } from "@cloudflare/kumo"; +import { useLingui } from "@lingui/react/macro"; import { Upload, Image, SquaresFour, List, MagnifyingGlass, Check, X } from "@phosphor-icons/react"; import { useQuery } from "@tanstack/react-query"; import * as React from "react"; diff --git a/packages/admin/src/components/MediaPickerModal.tsx b/packages/admin/src/components/MediaPickerModal.tsx index ffc6d3d61..6e5fc7e98 100644 --- a/packages/admin/src/components/MediaPickerModal.tsx +++ b/packages/admin/src/components/MediaPickerModal.tsx @@ -6,8 +6,8 @@ * Used by the rich text editor and image field components. */ -import { useLingui } from "@lingui/react/macro"; import { Button, Dialog, Input, Label, Loader } from "@cloudflare/kumo"; +import { useLingui } from "@lingui/react/macro"; import { Upload, Image, Check, Globe, MagnifyingGlass } from "@phosphor-icons/react"; import { X } from "@phosphor-icons/react"; import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; diff --git a/packages/admin/src/components/PortableTextEditor.tsx b/packages/admin/src/components/PortableTextEditor.tsx index 565cb101b..bdb5c2640 100644 --- a/packages/admin/src/components/PortableTextEditor.tsx +++ b/packages/admin/src/components/PortableTextEditor.tsx @@ -11,11 +11,11 @@ * - Floating menu on empty lines */ -import { t } from "@lingui/core/macro"; -import { useLingui } from "@lingui/react/macro"; 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 { t } from "@lingui/core/macro"; +import { useLingui } from "@lingui/react/macro"; import { TextB, TextItalic, diff --git a/packages/admin/src/components/WelcomeModal.tsx b/packages/admin/src/components/WelcomeModal.tsx index 420642db0..13d4d395e 100644 --- a/packages/admin/src/components/WelcomeModal.tsx +++ b/packages/admin/src/components/WelcomeModal.tsx @@ -4,8 +4,8 @@ * Shown to new users on their first login to welcome them to EmDash. */ -import { t } from "@lingui/core/macro"; import { Button, Dialog } from "@cloudflare/kumo"; +import { t } from "@lingui/core/macro"; import { X } from "@phosphor-icons/react"; import { useMutation, useQueryClient } from "@tanstack/react-query"; import * as React from "react"; diff --git a/packages/admin/src/components/Widgets.tsx b/packages/admin/src/components/Widgets.tsx index be084f1b2..7d46bf7b6 100644 --- a/packages/admin/src/components/Widgets.tsx +++ b/packages/admin/src/components/Widgets.tsx @@ -6,7 +6,6 @@ * Widgets within an area can be reordered via drag-and-drop. */ -import { t } from "@lingui/core/macro"; import { Button, Dialog, Input, Label, Select, Switch, Toast } from "@cloudflare/kumo"; import { DndContext, @@ -30,6 +29,7 @@ import { useSortable, } from "@dnd-kit/sortable"; import { CSS } from "@dnd-kit/utilities"; +import { t } from "@lingui/core/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"; diff --git a/packages/admin/src/components/editor/BlockMenu.tsx b/packages/admin/src/components/editor/BlockMenu.tsx index b039f42d0..f4384397a 100644 --- a/packages/admin/src/components/editor/BlockMenu.tsx +++ b/packages/admin/src/components/editor/BlockMenu.tsx @@ -10,9 +10,9 @@ * Uses Floating UI for positioning relative to the selected block. */ -import { t } from "@lingui/core/macro"; import { Button } from "@cloudflare/kumo"; import { useFloating, offset, flip, shift, autoUpdate } from "@floating-ui/react"; +import { t } from "@lingui/core/macro"; import { DotsSixVertical, Paragraph, diff --git a/packages/admin/src/components/settings/AllowedDomainsSettings.tsx b/packages/admin/src/components/settings/AllowedDomainsSettings.tsx index 2b1fb6cbb..0d000820b 100644 --- a/packages/admin/src/components/settings/AllowedDomainsSettings.tsx +++ b/packages/admin/src/components/settings/AllowedDomainsSettings.tsx @@ -5,8 +5,8 @@ * is configured, this page shows an informational message instead. */ -import { t } from "@lingui/core/macro"; import { Button, Dialog, Input, Select, Switch } from "@cloudflare/kumo"; +import { t } from "@lingui/core/macro"; import { Globe, Plus, diff --git a/packages/admin/src/components/settings/ApiTokenSettings.tsx b/packages/admin/src/components/settings/ApiTokenSettings.tsx index 30779ccca..5284f5a6d 100644 --- a/packages/admin/src/components/settings/ApiTokenSettings.tsx +++ b/packages/admin/src/components/settings/ApiTokenSettings.tsx @@ -4,8 +4,8 @@ * Allows admins to list, create, and revoke Personal Access Tokens. */ -import { t } from "@lingui/core/macro"; import { Button, Checkbox, Input, Loader, Select } from "@cloudflare/kumo"; +import { t } from "@lingui/core/macro"; import { ArrowLeft, Copy, diff --git a/packages/admin/src/components/users/RoleBadge.tsx b/packages/admin/src/components/users/RoleBadge.tsx index 6a45312e4..abbd274b5 100644 --- a/packages/admin/src/components/users/RoleBadge.tsx +++ b/packages/admin/src/components/users/RoleBadge.tsx @@ -1,4 +1,5 @@ import { t } from "@lingui/core/macro"; + import { cn } from "../../lib/utils"; /** Role level to name mapping */ diff --git a/packages/admin/src/lib/api/api-tokens.ts b/packages/admin/src/lib/api/api-tokens.ts index f447f4f93..bd44287b1 100644 --- a/packages/admin/src/lib/api/api-tokens.ts +++ b/packages/admin/src/lib/api/api-tokens.ts @@ -3,6 +3,7 @@ */ import { t } from "@lingui/core/macro"; + import { API_BASE, apiFetch, parseApiResponse, throwResponseError } from "./client.js"; // ============================================================================= @@ -39,7 +40,11 @@ export interface CreateApiTokenInput { /** Available scopes for API tokens */ export const API_TOKEN_SCOPES = [ { value: "content:read", label: t`Content Read`, description: t`Read content entries` }, - { value: "content:write", label: t`Content Write`, description: t`Create, update, delete content` }, + { + value: "content:write", + label: t`Content Write`, + description: t`Create, update, delete content`, + }, { value: "media:read", label: t`Media Read`, description: t`Read media files` }, { value: "media:write", label: t`Media Write`, description: t`Upload and delete media` }, { value: "schema:read", label: t`Schema Read`, description: t`Read collection schemas` }, diff --git a/packages/admin/src/locales/en/messages.mjs.d.ts b/packages/admin/src/locales/en/messages.mjs.d.ts index 1b8c01899..3bc87f1f5 100644 --- a/packages/admin/src/locales/en/messages.mjs.d.ts +++ b/packages/admin/src/locales/en/messages.mjs.d.ts @@ -1,5 +1,5 @@ declare module "*/messages.mjs" { import type { Messages } from "@lingui/core"; - + export const messages: Messages; } diff --git a/packages/admin/src/locales/init.ts b/packages/admin/src/locales/init.ts index 487adf1da..5f1c7c48e 100644 --- a/packages/admin/src/locales/init.ts +++ b/packages/admin/src/locales/init.ts @@ -1,12 +1,13 @@ /** * Pre-initialize i18n with English locale. - * + * * This MUST be imported first in index.ts to ensure i18n is ready * before any other module executes module-level t`...` calls. - * + * * Side-effect import - modifies global i18n instance. */ import { i18n } from "@lingui/core"; + import { messages } from "./en/messages.mjs"; if (!i18n.locale) { From 76389198f2092112f48a3ed02773b8fdd6e20d2f Mon Sep 17 00:00:00 2001 From: Ophir Bucai Date: Sun, 12 Apr 2026 02:06:03 +0300 Subject: [PATCH 06/48] Revert non-module-level translations from MediaLibrary and MediaPickerModal These components use runtime t from useLingui() inside useMemo, which is standard React i18n, not module-level extraction. Removing them from this PR as they're out of scope. --- packages/admin/src/components/MediaLibrary.tsx | 6 ++---- packages/admin/src/components/MediaPickerModal.tsx | 6 ++---- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/packages/admin/src/components/MediaLibrary.tsx b/packages/admin/src/components/MediaLibrary.tsx index f821b1679..76f5bca19 100644 --- a/packages/admin/src/components/MediaLibrary.tsx +++ b/packages/admin/src/components/MediaLibrary.tsx @@ -1,5 +1,4 @@ import { Button, Input, Loader } from "@cloudflare/kumo"; -import { useLingui } from "@lingui/react/macro"; import { Upload, Image, SquaresFour, List, MagnifyingGlass, Check, X } from "@phosphor-icons/react"; import { useQuery } from "@tanstack/react-query"; import * as React from "react"; @@ -35,7 +34,6 @@ export function MediaLibrary({ onDelete, onItemUpdated, }: MediaLibraryProps) { - const { i18n, t } = useLingui(); const [viewMode, setViewMode] = React.useState<"grid" | "list">("grid"); const [selectedItem, setSelectedItem] = React.useState(null); const [activeProvider, setActiveProvider] = React.useState("local"); @@ -198,7 +196,7 @@ export function MediaLibrary({ // Build provider tabs const providerTabs = React.useMemo(() => { const tabs: Array<{ id: string; name: string; icon?: string }> = [ - { id: "local", name: t`Library`, icon: undefined }, + { id: "local", name: "Library", icon: undefined }, ]; if (providers) { for (const p of providers) { @@ -208,7 +206,7 @@ export function MediaLibrary({ } } return tabs; - }, [providers, i18n.locale, t]); + }, [providers]); // Get current items based on active provider const currentItems = activeProvider === "local" ? items : []; diff --git a/packages/admin/src/components/MediaPickerModal.tsx b/packages/admin/src/components/MediaPickerModal.tsx index 6e5fc7e98..627c33f3b 100644 --- a/packages/admin/src/components/MediaPickerModal.tsx +++ b/packages/admin/src/components/MediaPickerModal.tsx @@ -7,7 +7,6 @@ */ import { Button, Dialog, Input, Label, Loader } from "@cloudflare/kumo"; -import { useLingui } from "@lingui/react/macro"; import { Upload, Image, Check, Globe, MagnifyingGlass } from "@phosphor-icons/react"; import { X } from "@phosphor-icons/react"; import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; @@ -66,7 +65,6 @@ export function MediaPickerModal({ mimeTypeFilter = "image/", title = "Select Image", }: MediaPickerModalProps) { - const { i18n, t } = useLingui(); const queryClient = useQueryClient(); const [selectedItem, setSelectedItem] = React.useState(null); const [activeProvider, setActiveProvider] = React.useState("local"); @@ -324,7 +322,7 @@ export function MediaPickerModal({ // Filter out "local" from API response since we add it manually const providerTabs = React.useMemo(() => { const tabs: Array<{ id: string; name: string; icon?: string }> = [ - { id: "local", name: t`Library`, icon: undefined }, + { id: "local", name: "Library", icon: undefined }, ]; if (providers) { for (const p of providers) { @@ -334,7 +332,7 @@ export function MediaPickerModal({ } } return tabs; - }, [providers, i18n.locale, t]); + }, [providers]); return ( From f96e7c480132f4dd0ce84a3de125b0fb8bdbfb85 Mon Sep 17 00:00:00 2001 From: Ophir Bucai Date: Sun, 12 Apr 2026 02:06:19 +0300 Subject: [PATCH 07/48] Re-extract locale catalogs after removing non-module-level strings Removed 'Library' string that was erroneously included from MediaLibrary and MediaPickerModal (runtime t usage, not module-level). --- packages/admin/src/locales/de/messages.po | 57 +++++++++++------------ packages/admin/src/locales/en/messages.po | 57 +++++++++++------------ 2 files changed, 52 insertions(+), 62 deletions(-) diff --git a/packages/admin/src/locales/de/messages.po b/packages/admin/src/locales/de/messages.po index 3f40dea5f..65e693f29 100644 --- a/packages/admin/src/locales/de/messages.po +++ b/packages/admin/src/locales/de/messages.po @@ -41,8 +41,8 @@ msgstr "" msgid "90 days" msgstr "" -#: packages/admin/src/components/users/RoleBadge.tsx:27 -#: packages/admin/src/lib/api/api-tokens.ts:47 +#: packages/admin/src/components/users/RoleBadge.tsx:28 +#: packages/admin/src/lib/api/api-tokens.ts:52 msgid "Admin" msgstr "" @@ -67,7 +67,7 @@ msgid "Authentication error: {error}" msgstr "Authentifizierungsfehler: {error}" #: packages/admin/src/components/settings/AllowedDomainsSettings.tsx:37 -#: packages/admin/src/components/users/RoleBadge.tsx:17 +#: packages/admin/src/components/users/RoleBadge.tsx:18 #: packages/admin/src/components/WelcomeModal.tsx:27 msgid "Author" msgstr "" @@ -82,19 +82,19 @@ msgstr "Zurück zur Anmeldung" msgid "Bullet List" msgstr "" -#: packages/admin/src/components/users/RoleBadge.tsx:14 +#: packages/admin/src/components/users/RoleBadge.tsx:15 msgid "Can create content" msgstr "" -#: packages/admin/src/components/users/RoleBadge.tsx:24 +#: packages/admin/src/components/users/RoleBadge.tsx:25 msgid "Can manage all content" msgstr "" -#: packages/admin/src/components/users/RoleBadge.tsx:19 +#: packages/admin/src/components/users/RoleBadge.tsx:20 msgid "Can publish own content" msgstr "" -#: packages/admin/src/components/users/RoleBadge.tsx:9 +#: packages/admin/src/components/users/RoleBadge.tsx:10 msgid "Can view content" msgstr "" @@ -128,7 +128,7 @@ msgstr "" msgid "Content Block" msgstr "" -#: packages/admin/src/lib/api/api-tokens.ts:41 +#: packages/admin/src/lib/api/api-tokens.ts:42 msgid "Content Read" msgstr "" @@ -136,12 +136,12 @@ msgstr "" msgid "Content Types" msgstr "" -#: packages/admin/src/lib/api/api-tokens.ts:42 +#: packages/admin/src/lib/api/api-tokens.ts:45 msgid "Content Write" msgstr "" #: packages/admin/src/components/settings/AllowedDomainsSettings.tsx:36 -#: packages/admin/src/components/users/RoleBadge.tsx:12 +#: packages/admin/src/components/users/RoleBadge.tsx:13 #: packages/admin/src/components/WelcomeModal.tsx:28 msgid "Contributor" msgstr "" @@ -158,7 +158,7 @@ msgstr "" msgid "Create personal access tokens for programmatic API access" msgstr "Persönliche Zugangstokens für programmatischen API-Zugriff erstellen" -#: packages/admin/src/lib/api/api-tokens.ts:42 +#: packages/admin/src/lib/api/api-tokens.ts:46 msgid "Create, update, delete content" msgstr "" @@ -191,7 +191,7 @@ msgid "Drafts" msgstr "" #: packages/admin/src/components/settings/AllowedDomainsSettings.tsx:38 -#: packages/admin/src/components/users/RoleBadge.tsx:22 +#: packages/admin/src/components/users/RoleBadge.tsx:23 #: packages/admin/src/components/WelcomeModal.tsx:26 msgid "Editor" msgstr "" @@ -222,11 +222,11 @@ msgstr "" msgid "Failed to send magic link" msgstr "Magic Link konnte nicht gesendet werden" -#: packages/admin/src/components/users/RoleBadge.tsx:29 +#: packages/admin/src/components/users/RoleBadge.tsx:30 msgid "Full access" msgstr "" -#: packages/admin/src/lib/api/api-tokens.ts:47 +#: packages/admin/src/lib/api/api-tokens.ts:52 msgid "Full admin access" msgstr "" @@ -293,11 +293,6 @@ msgstr "Sprache" msgid "Large section heading" msgstr "" -#: packages/admin/src/components/MediaLibrary.tsx:201 -#: packages/admin/src/components/MediaPickerModal.tsx:327 -msgid "Library" -msgstr "" - #: packages/admin/src/components/LocaleSwitcher.tsx:60 msgid "Locale" msgstr "Sprache" @@ -314,11 +309,11 @@ msgstr "" msgid "Media Library" msgstr "" -#: packages/admin/src/lib/api/api-tokens.ts:43 +#: packages/admin/src/lib/api/api-tokens.ts:48 msgid "Media Read" msgstr "" -#: packages/admin/src/lib/api/api-tokens.ts:44 +#: packages/admin/src/lib/api/api-tokens.ts:49 msgid "Media Write" msgstr "" @@ -335,7 +330,7 @@ msgstr "" msgid "Menus" msgstr "" -#: packages/admin/src/lib/api/api-tokens.ts:46 +#: packages/admin/src/lib/api/api-tokens.ts:51 msgid "Modify collection schemas" msgstr "" @@ -377,15 +372,15 @@ msgstr "" msgid "Quote" msgstr "" -#: packages/admin/src/lib/api/api-tokens.ts:45 +#: packages/admin/src/lib/api/api-tokens.ts:50 msgid "Read collection schemas" msgstr "" -#: packages/admin/src/lib/api/api-tokens.ts:41 +#: packages/admin/src/lib/api/api-tokens.ts:42 msgid "Read content entries" msgstr "" -#: packages/admin/src/lib/api/api-tokens.ts:43 +#: packages/admin/src/lib/api/api-tokens.ts:48 msgid "Read media files" msgstr "" @@ -397,7 +392,7 @@ msgstr "" msgid "Rich text content" msgstr "" -#: packages/admin/src/components/users/RoleBadge.tsx:37 +#: packages/admin/src/components/users/RoleBadge.tsx:38 msgid "Role {role}" msgstr "" @@ -405,11 +400,11 @@ msgstr "" msgid "Save content as draft before publishing" msgstr "" -#: packages/admin/src/lib/api/api-tokens.ts:45 +#: packages/admin/src/lib/api/api-tokens.ts:50 msgid "Schema Read" msgstr "" -#: packages/admin/src/lib/api/api-tokens.ts:46 +#: packages/admin/src/lib/api/api-tokens.ts:51 msgid "Schema Write" msgstr "" @@ -499,7 +494,7 @@ msgid "Status" msgstr "" #: packages/admin/src/components/settings/AllowedDomainsSettings.tsx:35 -#: packages/admin/src/components/users/RoleBadge.tsx:7 +#: packages/admin/src/components/users/RoleBadge.tsx:8 #: packages/admin/src/components/WelcomeModal.tsx:29 msgid "Subscriber" msgstr "" @@ -524,7 +519,7 @@ msgstr "" msgid "Unknown" msgstr "" -#: packages/admin/src/components/users/RoleBadge.tsx:39 +#: packages/admin/src/components/users/RoleBadge.tsx:40 msgid "Unknown role" msgstr "" @@ -532,7 +527,7 @@ msgstr "" msgid "Updated At" msgstr "" -#: packages/admin/src/lib/api/api-tokens.ts:44 +#: packages/admin/src/lib/api/api-tokens.ts:49 msgid "Upload and delete media" msgstr "" diff --git a/packages/admin/src/locales/en/messages.po b/packages/admin/src/locales/en/messages.po index 6306a75c5..7cdb5fd7d 100644 --- a/packages/admin/src/locales/en/messages.po +++ b/packages/admin/src/locales/en/messages.po @@ -41,8 +41,8 @@ msgstr "7 days" msgid "90 days" msgstr "90 days" -#: packages/admin/src/components/users/RoleBadge.tsx:27 -#: packages/admin/src/lib/api/api-tokens.ts:47 +#: packages/admin/src/components/users/RoleBadge.tsx:28 +#: packages/admin/src/lib/api/api-tokens.ts:52 msgid "Admin" msgstr "Admin" @@ -67,7 +67,7 @@ msgid "Authentication error: {error}" msgstr "Authentication error: {error}" #: packages/admin/src/components/settings/AllowedDomainsSettings.tsx:37 -#: packages/admin/src/components/users/RoleBadge.tsx:17 +#: packages/admin/src/components/users/RoleBadge.tsx:18 #: packages/admin/src/components/WelcomeModal.tsx:27 msgid "Author" msgstr "Author" @@ -82,19 +82,19 @@ msgstr "Back to login" msgid "Bullet List" msgstr "Bullet List" -#: packages/admin/src/components/users/RoleBadge.tsx:14 +#: packages/admin/src/components/users/RoleBadge.tsx:15 msgid "Can create content" msgstr "Can create content" -#: packages/admin/src/components/users/RoleBadge.tsx:24 +#: packages/admin/src/components/users/RoleBadge.tsx:25 msgid "Can manage all content" msgstr "Can manage all content" -#: packages/admin/src/components/users/RoleBadge.tsx:19 +#: packages/admin/src/components/users/RoleBadge.tsx:20 msgid "Can publish own content" msgstr "Can publish own content" -#: packages/admin/src/components/users/RoleBadge.tsx:9 +#: packages/admin/src/components/users/RoleBadge.tsx:10 msgid "Can view content" msgstr "Can view content" @@ -128,7 +128,7 @@ msgstr "Content" msgid "Content Block" msgstr "Content Block" -#: packages/admin/src/lib/api/api-tokens.ts:41 +#: packages/admin/src/lib/api/api-tokens.ts:42 msgid "Content Read" msgstr "Content Read" @@ -136,12 +136,12 @@ msgstr "Content Read" msgid "Content Types" msgstr "Content Types" -#: packages/admin/src/lib/api/api-tokens.ts:42 +#: packages/admin/src/lib/api/api-tokens.ts:45 msgid "Content Write" msgstr "Content Write" #: packages/admin/src/components/settings/AllowedDomainsSettings.tsx:36 -#: packages/admin/src/components/users/RoleBadge.tsx:12 +#: packages/admin/src/components/users/RoleBadge.tsx:13 #: packages/admin/src/components/WelcomeModal.tsx:28 msgid "Contributor" msgstr "Contributor" @@ -158,7 +158,7 @@ msgstr "Create a numbered list" msgid "Create personal access tokens for programmatic API access" msgstr "Create personal access tokens for programmatic API access" -#: packages/admin/src/lib/api/api-tokens.ts:42 +#: packages/admin/src/lib/api/api-tokens.ts:46 msgid "Create, update, delete content" msgstr "Create, update, delete content" @@ -191,7 +191,7 @@ msgid "Drafts" msgstr "Drafts" #: packages/admin/src/components/settings/AllowedDomainsSettings.tsx:38 -#: packages/admin/src/components/users/RoleBadge.tsx:22 +#: packages/admin/src/components/users/RoleBadge.tsx:23 #: packages/admin/src/components/WelcomeModal.tsx:26 msgid "Editor" msgstr "Editor" @@ -222,11 +222,11 @@ msgstr "Enable full-text search on this collection" msgid "Failed to send magic link" msgstr "Failed to send magic link" -#: packages/admin/src/components/users/RoleBadge.tsx:29 +#: packages/admin/src/components/users/RoleBadge.tsx:30 msgid "Full access" msgstr "Full access" -#: packages/admin/src/lib/api/api-tokens.ts:47 +#: packages/admin/src/lib/api/api-tokens.ts:52 msgid "Full admin access" msgstr "Full admin access" @@ -293,11 +293,6 @@ msgstr "Language" msgid "Large section heading" msgstr "Large section heading" -#: packages/admin/src/components/MediaLibrary.tsx:201 -#: packages/admin/src/components/MediaPickerModal.tsx:327 -msgid "Library" -msgstr "Library" - #: packages/admin/src/components/LocaleSwitcher.tsx:60 msgid "Locale" msgstr "Locale" @@ -314,11 +309,11 @@ msgstr "Media" msgid "Media Library" msgstr "Media Library" -#: packages/admin/src/lib/api/api-tokens.ts:43 +#: packages/admin/src/lib/api/api-tokens.ts:48 msgid "Media Read" msgstr "Media Read" -#: packages/admin/src/lib/api/api-tokens.ts:44 +#: packages/admin/src/lib/api/api-tokens.ts:49 msgid "Media Write" msgstr "Media Write" @@ -335,7 +330,7 @@ msgstr "Menu" msgid "Menus" msgstr "Menus" -#: packages/admin/src/lib/api/api-tokens.ts:46 +#: packages/admin/src/lib/api/api-tokens.ts:51 msgid "Modify collection schemas" msgstr "Modify collection schemas" @@ -377,15 +372,15 @@ msgstr "Published At" msgid "Quote" msgstr "Quote" -#: packages/admin/src/lib/api/api-tokens.ts:45 +#: packages/admin/src/lib/api/api-tokens.ts:50 msgid "Read collection schemas" msgstr "Read collection schemas" -#: packages/admin/src/lib/api/api-tokens.ts:41 +#: packages/admin/src/lib/api/api-tokens.ts:42 msgid "Read content entries" msgstr "Read content entries" -#: packages/admin/src/lib/api/api-tokens.ts:43 +#: packages/admin/src/lib/api/api-tokens.ts:48 msgid "Read media files" msgstr "Read media files" @@ -397,7 +392,7 @@ msgstr "Revisions" msgid "Rich text content" msgstr "Rich text content" -#: packages/admin/src/components/users/RoleBadge.tsx:37 +#: packages/admin/src/components/users/RoleBadge.tsx:38 msgid "Role {role}" msgstr "Role {role}" @@ -405,11 +400,11 @@ msgstr "Role {role}" msgid "Save content as draft before publishing" msgstr "Save content as draft before publishing" -#: packages/admin/src/lib/api/api-tokens.ts:45 +#: packages/admin/src/lib/api/api-tokens.ts:50 msgid "Schema Read" msgstr "Schema Read" -#: packages/admin/src/lib/api/api-tokens.ts:46 +#: packages/admin/src/lib/api/api-tokens.ts:51 msgid "Schema Write" msgstr "Schema Write" @@ -499,7 +494,7 @@ msgid "Status" msgstr "Status" #: packages/admin/src/components/settings/AllowedDomainsSettings.tsx:35 -#: packages/admin/src/components/users/RoleBadge.tsx:7 +#: packages/admin/src/components/users/RoleBadge.tsx:8 #: packages/admin/src/components/WelcomeModal.tsx:29 msgid "Subscriber" msgstr "Subscriber" @@ -524,7 +519,7 @@ msgstr "Unique identifier (ULID)" msgid "Unknown" msgstr "Unknown" -#: packages/admin/src/components/users/RoleBadge.tsx:39 +#: packages/admin/src/components/users/RoleBadge.tsx:40 msgid "Unknown role" msgstr "Unknown role" @@ -532,7 +527,7 @@ msgstr "Unknown role" msgid "Updated At" msgstr "Updated At" -#: packages/admin/src/lib/api/api-tokens.ts:44 +#: packages/admin/src/lib/api/api-tokens.ts:49 msgid "Upload and delete media" msgstr "Upload and delete media" From e53bf34905c9c5408e86f83a650afa2d4c6ccef4 Mon Sep 17 00:00:00 2001 From: Ophir Bucai Date: Sun, 12 Apr 2026 02:10:00 +0300 Subject: [PATCH 08/48] Clarify init.ts must be imported in both index.ts and admin.astro --- packages/admin/src/locales/init.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/admin/src/locales/init.ts b/packages/admin/src/locales/init.ts index 5f1c7c48e..56aec29c0 100644 --- a/packages/admin/src/locales/init.ts +++ b/packages/admin/src/locales/init.ts @@ -1,8 +1,12 @@ /** * Pre-initialize i18n with English locale. * - * This MUST be imported first in index.ts to ensure i18n is ready - * before any other module executes module-level t`...` calls. + * This MUST be imported in BOTH: + * 1. index.ts (client-side entry point) + * 2. admin.astro (server-side Astro route) + * + * This ensures i18n is initialized before any module-level t`...` calls + * execute, regardless of Astro's island hydration order. * * Side-effect import - modifies global i18n instance. */ From 4072071ef81bdcca152aa4b1a077a308c690c3b8 Mon Sep 17 00:00:00 2001 From: Ophir Bucai Date: Sun, 12 Apr 2026 02:12:43 +0300 Subject: [PATCH 09/48] Suppress lint warnings for intentional side-effect imports --- packages/admin/src/index.ts | 1 + packages/core/src/astro/routes/admin.astro | 1 + 2 files changed, 2 insertions(+) diff --git a/packages/admin/src/index.ts b/packages/admin/src/index.ts index 95dc765fb..18afdc68b 100644 --- a/packages/admin/src/index.ts +++ b/packages/admin/src/index.ts @@ -2,6 +2,7 @@ * CRITICAL: This must be imported FIRST to pre-initialize i18n * before any module-level t`...` calls execute. */ +// eslint-disable-next-line import/no-unassigned-import import "./locales/init.js"; // Main App diff --git a/packages/core/src/astro/routes/admin.astro b/packages/core/src/astro/routes/admin.astro index ac93495fd..a72b8868a 100644 --- a/packages/core/src/astro/routes/admin.astro +++ b/packages/core/src/astro/routes/admin.astro @@ -7,6 +7,7 @@ */ import "@emdash-cms/admin/styles.css"; // Pre-initialize i18n before any client modules load +// eslint-disable-next-line import/no-unassigned-import import "@emdash-cms/admin/locales/init"; // Use package-qualified import so Astro generates a proper module URL // (relative imports resolve to absolute paths which break client hydration) From 056af2808fb55e9cbfe3be7d8c4f4e7d06276abe Mon Sep 17 00:00:00 2001 From: Ophir Bucai Date: Sun, 12 Apr 2026 02:13:43 +0300 Subject: [PATCH 10/48] Simplify init.ts import comment in index.ts --- packages/admin/src/index.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/packages/admin/src/index.ts b/packages/admin/src/index.ts index 18afdc68b..171d75dfe 100644 --- a/packages/admin/src/index.ts +++ b/packages/admin/src/index.ts @@ -1,7 +1,4 @@ -/** - * CRITICAL: This must be imported FIRST to pre-initialize i18n - * before any module-level t`...` calls execute. - */ +// Pre-initialize i18n before any client modules load // eslint-disable-next-line import/no-unassigned-import import "./locales/init.js"; From c134614ca4f7bf628daf066efbfe08767c3ac290 Mon Sep 17 00:00:00 2001 From: Ophir Bucai Date: Sun, 12 Apr 2026 02:18:35 +0300 Subject: [PATCH 11/48] Add .js extension to init module import in admin.astro --- packages/core/src/astro/routes/admin.astro | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/src/astro/routes/admin.astro b/packages/core/src/astro/routes/admin.astro index a72b8868a..e626492c8 100644 --- a/packages/core/src/astro/routes/admin.astro +++ b/packages/core/src/astro/routes/admin.astro @@ -8,7 +8,7 @@ import "@emdash-cms/admin/styles.css"; // Pre-initialize i18n before any client modules load // eslint-disable-next-line import/no-unassigned-import -import "@emdash-cms/admin/locales/init"; +import "@emdash-cms/admin/locales/init.js"; // Use package-qualified import so Astro generates a proper module URL // (relative imports resolve to absolute paths which break client hydration) import AdminWrapper from "emdash/routes/PluginRegistry"; From 655282e4bd1583bc802c6c33f15d3b57f6b0a0af Mon Sep 17 00:00:00 2001 From: Ophir Bucai Date: Sun, 12 Apr 2026 02:24:15 +0300 Subject: [PATCH 12/48] Fix eager module-level t translations to be lazy/reactive Converted module-level constants with t\`\` calls to builder functions that return the config on demand. This ensures translations execute at render time (when locale is current) rather than at import time (frozen to pre-init locale). Changed: - RoleBadge: ROLE_CONFIG -> buildRoleConfig(), called in useMemo - api-tokens: API_TOKEN_SCOPES -> buildApiTokenScopes(), called in useMemo - ApiTokenSettings: EXPIRY_OPTIONS -> buildExpiryOptions(), called in useMemo All components using these now have i18n.locale in their useMemo dependencies, ensuring labels/descriptions update when locale changes. --- .../components/settings/ApiTokenSettings.tsx | 29 ++++--- .../admin/src/components/users/RoleBadge.tsx | 76 +++++++++++-------- packages/admin/src/lib/api/api-tokens.ts | 30 ++++---- packages/admin/src/lib/api/index.ts | 2 +- 4 files changed, 80 insertions(+), 57 deletions(-) diff --git a/packages/admin/src/components/settings/ApiTokenSettings.tsx b/packages/admin/src/components/settings/ApiTokenSettings.tsx index 5284f5a6d..d0110eb0f 100644 --- a/packages/admin/src/components/settings/ApiTokenSettings.tsx +++ b/packages/admin/src/components/settings/ApiTokenSettings.tsx @@ -6,6 +6,7 @@ import { Button, Checkbox, Input, Loader, Select } from "@cloudflare/kumo"; import { t } from "@lingui/core/macro"; +import { useLingui } from "@lingui/react/macro"; import { ArrowLeft, Copy, @@ -24,7 +25,7 @@ import { fetchApiTokens, createApiToken, revokeApiToken, - API_TOKEN_SCOPES, + buildApiTokenScopes, type ApiTokenCreateResult, } from "../../lib/api/api-tokens.js"; import { getMutationError } from "../DialogError.js"; @@ -33,13 +34,15 @@ import { getMutationError } from "../DialogError.js"; // Expiry options // ============================================================================= -const EXPIRY_OPTIONS = [ - { value: "none", label: t`No expiry` }, - { value: "7d", label: t`7 days` }, - { value: "30d", label: t`30 days` }, - { value: "90d", label: t`90 days` }, - { value: "365d", label: t`1 year` }, -] as const; +function buildExpiryOptions() { + return [ + { value: "none", label: t`No expiry` }, + { value: "7d", label: t`7 days` }, + { value: "30d", label: t`30 days` }, + { value: "90d", label: t`90 days` }, + { value: "365d", label: t`1 year` }, + ] as const; +} function computeExpiryDate(option: string): string | undefined { if (option === "none") return undefined; @@ -289,6 +292,10 @@ interface CreateTokenFormProps { } function CreateTokenForm({ isCreating, error, onSubmit, onCancel }: CreateTokenFormProps) { + const { i18n } = useLingui(); + const apiTokenScopes = React.useMemo(() => buildApiTokenScopes(), [i18n.locale]); + const expiryOptions = React.useMemo(() => buildExpiryOptions(), [i18n.locale]); + const [name, setName] = React.useState(""); const [selectedScopes, setSelectedScopes] = React.useState>(new Set()); const [expiry, setExpiry] = React.useState("30d"); @@ -340,7 +347,7 @@ function CreateTokenForm({ isCreating, error, onSubmit, onCancel }: CreateTokenF
Scopes
- {API_TOKEN_SCOPES.map((scope) => ( + {apiTokenScopes.map((scope) => (
- +
)) @@ -1350,7 +1350,8 @@ export function PortableTextEditor({ onBlockSidebarOpen, onBlockSidebarClose, }: PortableTextEditorProps) { - const { i18n } = useLingui(); + 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,16 +1400,16 @@ export function PortableTextEditor({ // Build slash commands const slashCommands = React.useMemo(() => { - const cmds: SlashCommandItem[] = [...buildDefaultSlashCommands()]; + const cmds: SlashCommandItem[] = [...defaultSlashCommands]; // Add image command cmds.push({ id: "image", - title: t`Image`, - description: t`Insert an image`, + title: msg`Image`, + description: msg`Insert an image`, icon: ImageIcon, aliases: ["img", "photo", "picture", "url"], - category: t`Media`, + category: msg`Media`, command: ({ editor, range }) => { editor.chain().focus().deleteRange(range).run(); setMediaPickerOpen(true); @@ -1418,11 +1419,11 @@ export function PortableTextEditor({ // Add section command cmds.push({ id: "section", - title: t`Section`, - description: t`Insert a reusable section`, + title: msg`Section`, + description: msg`Insert a reusable section`, icon: Stack, aliases: ["pattern", "block", "template"], - category: t`Content`, + category: msg`Content`, command: ({ editor, range }) => { editor.chain().focus().deleteRange(range).run(); setSectionPickerOpen(true); @@ -1431,13 +1432,16 @@ export function PortableTextEditor({ // Add plugin block commands for (const block of pluginBlocks) { + const labelLower = block.label.toLowerCase(); cmds.push({ id: `plugin-${block.pluginId}-${block.type}`, - title: block.label, - description: block.description || t`Embed a ${block.label.toLowerCase()}`, + title: msg`${block.label}`, + description: block.description + ? msg`${block.description}` + : msg`Embed a ${labelLower}`, icon: resolveIcon(block.icon), aliases: [block.type], - category: t`Embeds`, + category: msg`Embeds`, command: ({ editor, range }) => { editor.chain().focus().deleteRange(range).run(); setPluginBlockModal(block); @@ -1446,7 +1450,7 @@ export function PortableTextEditor({ } return cmds; - }, [pluginBlocks, i18n.locale]); + }, [pluginBlocks, t]); // Filter commands by query — accessed via ref so the Suggestion plugin // (created once) always sees the latest command list without needing @@ -1458,10 +1462,10 @@ export function PortableTextEditor({ const titleMatches: SlashCommandItem[] = []; const otherMatches: SlashCommandItem[] = []; for (const item of slashCommands) { - if (item.title.toLowerCase().includes(searchText)) { + if (t(item.title).toLowerCase().includes(searchText)) { titleMatches.push(item); } else if ( - item.description.toLowerCase().includes(searchText) || + t(item.description).toLowerCase().includes(searchText) || item.aliases?.some((alias) => alias.toLowerCase().includes(searchText)) ) { otherMatches.push(item); diff --git a/packages/admin/tests/editor/PortableTextEditor.test.tsx b/packages/admin/tests/editor/PortableTextEditor.test.tsx index 2c7a4be5d..0eb7bdf44 100644 --- a/packages/admin/tests/editor/PortableTextEditor.test.tsx +++ b/packages/admin/tests/editor/PortableTextEditor.test.tsx @@ -10,10 +10,10 @@ import type { Editor } from "@tiptap/react"; import * as React from "react"; import { describe, it, expect, vi } from "vitest"; +import { render } from "../utils/render"; + import type { PluginBlockDef } from "../../src/components/PortableTextEditor"; import { PortableTextEditor } from "../../src/components/PortableTextEditor"; -import { render } from "../utils/render.tsx"; -import { TestWrapper } from "../utils/test-helpers.js"; // --------------------------------------------------------------------------- // Mocks — heavy components that need network / Astro context @@ -131,7 +131,6 @@ async function renderAndGetEditor(props: Partial, - { wrapper: TestWrapper }, ); const pm = await waitForEditor(); await vi.waitFor(() => expect(capturedEditor).toBeTruthy(), { timeout: 2000 }); @@ -180,9 +179,7 @@ function textBlock( describe("Portable Text ↔ ProseMirror conversion", () => { it("renders a paragraph from PT value", async () => { - await render(, { - wrapper: TestWrapper, - }); + await render(); const pm = await waitForEditor(); const p = pm.querySelector("p"); expect(p).toBeTruthy(); From 11bc3001850c8f3550c6c7973e0e35f660497de7 Mon Sep 17 00:00:00 2001 From: Ophir Bucai Date: Sun, 12 Apr 2026 12:38:59 +0300 Subject: [PATCH 32/48] refactor(admin): migrate WelcomeModal to msg and useLingui Module-level msg for roles, scope copy, titles, and actions; interpolated welcome title with first name. Tests use shared render for I18nProvider. --- .../admin/src/components/WelcomeModal.tsx | 80 +++++++++++-------- .../tests/components/WelcomeModal.test.tsx | 3 +- 2 files changed, 48 insertions(+), 35 deletions(-) diff --git a/packages/admin/src/components/WelcomeModal.tsx b/packages/admin/src/components/WelcomeModal.tsx index 86bed34b7..68340690e 100644 --- a/packages/admin/src/components/WelcomeModal.tsx +++ b/packages/admin/src/components/WelcomeModal.tsx @@ -5,7 +5,8 @@ */ import { Button, Dialog } from "@cloudflare/kumo"; -import { t } from "@lingui/core/macro"; +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"; @@ -21,17 +22,37 @@ interface WelcomeModalProps { userRole: number; } -// Role labels -function buildGetRoleLabel() { - return (role: number): string => { - if (role >= 50) return t`Administrator`; - if (role >= 40) return t`Editor`; - if (role >= 30) return t`Author`; - if (role >= 20) return t`Contributor`; - return t`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", @@ -42,9 +63,7 @@ async function dismissWelcome(): Promise { } export function WelcomeModal({ open, onClose, userName, userRole }: WelcomeModalProps) { - const { i18n } = useLingui(); - const getRoleLabel = React.useMemo(() => buildGetRoleLabel(), [i18n.locale]); - + const { t } = useLingui(); const queryClient = useQueryClient(); const dismissMutation = useMutation({ @@ -69,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()}>
( )} /> @@ -98,39 +121,28 @@ 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/tests/components/WelcomeModal.test.tsx b/packages/admin/tests/components/WelcomeModal.test.tsx index 4a850038e..ef4c2310b 100644 --- a/packages/admin/tests/components/WelcomeModal.test.tsx +++ b/packages/admin/tests/components/WelcomeModal.test.tsx @@ -2,8 +2,9 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import * as React from "react"; import { describe, it, expect, vi } from "vitest"; +import { render } from "../utils/render"; + import { WelcomeModal } from "../../src/components/WelcomeModal"; -import { render } from "../utils/render.tsx"; // --------------------------------------------------------------------------- // Constants From 1eeccf3c204ebf745c547b4475900ed100c9993e Mon Sep 17 00:00:00 2001 From: Ophir Bucai Date: Sun, 12 Apr 2026 13:16:16 +0300 Subject: [PATCH 33/48] refactor(admin): migrate Widgets built-in palette to msg and useLingui BUILTIN_WIDGETS label and description use module-level msg; Widgets resolves them with useLingui t() for palette rows and drag payload labels. --- packages/admin/src/components/Widgets.tsx | 47 +++++++++++------------ 1 file changed, 22 insertions(+), 25 deletions(-) diff --git a/packages/admin/src/components/Widgets.tsx b/packages/admin/src/components/Widgets.tsx index 12d5f40bc..b05731c6c 100644 --- a/packages/admin/src/components/Widgets.tsx +++ b/packages/admin/src/components/Widgets.tsx @@ -29,7 +29,8 @@ import { useSortable, } from "@dnd-kit/sortable"; import { CSS } from "@dnd-kit/utilities"; -import { t } from "@lingui/core/macro"; +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"; @@ -76,32 +77,28 @@ function isPaletteItem(data: DragItemData): data is PaletteItemData { } /** Built-in widget types available in the palette */ -function buildBuiltinWidgets(): Array<{ +const BUILTIN_WIDGETS: Array<{ id: string; - label: string; - description: string; + label: MessageDescriptor; + description: MessageDescriptor; input: CreateWidgetInput; -}> { - return [ - { - id: "palette-content", - label: t`Content Block`, - description: t`Rich text content`, - input: { type: "content", title: t`Content Block` }, - }, - { - id: "palette-menu", - label: t`Menu`, - description: t`Display a navigation menu`, - input: { type: "menu", title: t`Menu` }, - }, - ]; -} +}> = [ + { + id: "palette-content", + label: msg`Content Block`, + description: msg`Rich text content`, + input: { type: "content", title: "Content Block" }, + }, + { + id: "palette-menu", + label: msg`Menu`, + description: msg`Display a navigation menu`, + input: { type: "menu", title: "Menu" }, + }, +]; export function Widgets() { - const { i18n } = useLingui(); - const BUILTIN_WIDGETS = React.useMemo(() => buildBuiltinWidgets(), [i18n.locale]); - + const { t } = useLingui(); const queryClient = useQueryClient(); const toastManager = Toast.useToastManager(); const [isCreateAreaOpen, setIsCreateAreaOpen] = React.useState(false); @@ -377,8 +374,8 @@ export function Widgets() { ))} From 9b083dee9cc7de6a64cdf7a3634ff45a9d33375f Mon Sep 17 00:00:00 2001 From: Ophir Bucai Date: Sun, 12 Apr 2026 14:15:53 +0300 Subject: [PATCH 34/48] refactor(admin): simplify command palette nav item titles Use `title: string | MessageDescriptor` on `NavItem` and resolve with inline `typeof` checks for filtering and palette rows. Removes the old discriminated wrapper and helper indirection (KISS, easier review). --- .../src/components/AdminCommandPalette.tsx | 54 +++++++------------ 1 file changed, 19 insertions(+), 35 deletions(-) diff --git a/packages/admin/src/components/AdminCommandPalette.tsx b/packages/admin/src/components/AdminCommandPalette.tsx index f7a58ebd6..5d9400726 100644 --- a/packages/admin/src/components/AdminCommandPalette.tsx +++ b/packages/admin/src/components/AdminCommandPalette.tsx @@ -50,23 +50,6 @@ const SEARCH_DEBOUNCE_MS = 300; // Detect macOS for keyboard shortcut display const IS_MAC = typeof navigator !== "undefined" && /Mac|iPhone|iPad|iPod/.test(navigator.userAgent); -/** - * Nav entry title: either a Lingui message descriptor (static copy) or a plain - * string from the manifest (collections / plugin pages). The macro does not - * support dynamic `msg({ id: … + runtime, message })` for extract, so API - * labels stay as `api` until a dedicated catalog pattern exists. - */ -type NavItemTitle = - | { kind: "i18n"; descriptor: MessageDescriptor } - | { kind: "api"; label: string }; - -function navItemTitleText( - title: NavItemTitle, - translate: (d: MessageDescriptor) => string, -): string { - return title.kind === "api" ? title.label : translate(title.descriptor); -} - /** * Custom hook for debouncing a value */ @@ -101,7 +84,7 @@ interface SearchResponse { interface NavItem { id: string; - title: NavItemTitle; + title: string | MessageDescriptor; to: string; params?: Record; icon: React.ElementType; @@ -145,7 +128,7 @@ function buildNavItems(manifest: CommandPaletteManifest, userRole: number): NavI const items: NavItem[] = [ { id: "dashboard", - title: { kind: "i18n", descriptor: msg`Dashboard` }, + title: msg`Dashboard`, to: "/", icon: SquaresFour, keywords: ["home", "overview"], @@ -156,7 +139,7 @@ function buildNavItems(manifest: CommandPaletteManifest, userRole: number): NavI for (const [name, config] of Object.entries(manifest.collections)) { items.push({ id: `collection-${name}`, - title: { kind: "api", label: config.label }, + title: config.label, to: "/content/$collection", params: { collection: name }, icon: FileText, @@ -168,14 +151,14 @@ function buildNavItems(manifest: CommandPaletteManifest, userRole: number): NavI items.push( { id: "media", - title: { kind: "i18n", descriptor: msg`Media Library` }, + title: msg`Media Library`, to: "/media", icon: Image, keywords: ["images", "files", "uploads"], }, { id: "menus", - title: { kind: "i18n", descriptor: msg`Menus` }, + title: msg`Menus`, to: "/menus", icon: List, minRole: ROLE_EDITOR, @@ -183,7 +166,7 @@ function buildNavItems(manifest: CommandPaletteManifest, userRole: number): NavI }, { id: "widgets", - title: { kind: "i18n", descriptor: msg`Widgets` }, + title: msg`Widgets`, to: "/widgets", icon: GridFour, minRole: ROLE_EDITOR, @@ -191,7 +174,7 @@ function buildNavItems(manifest: CommandPaletteManifest, userRole: number): NavI }, { id: "sections", - title: { kind: "i18n", descriptor: msg`Sections` }, + title: msg`Sections`, to: "/sections", icon: Stack, minRole: ROLE_EDITOR, @@ -199,7 +182,7 @@ function buildNavItems(manifest: CommandPaletteManifest, userRole: number): NavI }, { id: "content-types", - title: { kind: "i18n", descriptor: msg`Content Types` }, + title: msg`Content Types`, to: "/content-types", icon: Database, minRole: ROLE_ADMIN, @@ -207,7 +190,7 @@ function buildNavItems(manifest: CommandPaletteManifest, userRole: number): NavI }, { id: "categories", - title: { kind: "i18n", descriptor: msg`Categories` }, + title: msg`Categories`, to: "/taxonomies/$taxonomy", params: { taxonomy: "category" }, icon: FileText, @@ -216,7 +199,7 @@ function buildNavItems(manifest: CommandPaletteManifest, userRole: number): NavI }, { id: "tags", - title: { kind: "i18n", descriptor: msg`Tags` }, + title: msg`Tags`, to: "/taxonomies/$taxonomy", params: { taxonomy: "tag" }, icon: FileText, @@ -225,7 +208,7 @@ function buildNavItems(manifest: CommandPaletteManifest, userRole: number): NavI }, { id: "users", - title: { kind: "i18n", descriptor: msg`Users` }, + title: msg`Users`, to: "/users", icon: Users, minRole: ROLE_ADMIN, @@ -233,7 +216,7 @@ function buildNavItems(manifest: CommandPaletteManifest, userRole: number): NavI }, { id: "plugins", - title: { kind: "i18n", descriptor: msg`Plugins` }, + title: msg`Plugins`, to: "/plugins-manager", icon: PuzzlePiece, minRole: ROLE_ADMIN, @@ -241,7 +224,7 @@ function buildNavItems(manifest: CommandPaletteManifest, userRole: number): NavI }, { id: "import", - title: { kind: "i18n", descriptor: msg`Import` }, + title: msg`Import`, to: "/import/wordpress", icon: Upload, minRole: ROLE_ADMIN, @@ -249,7 +232,7 @@ function buildNavItems(manifest: CommandPaletteManifest, userRole: number): NavI }, { id: "settings", - title: { kind: "i18n", descriptor: msg`Settings` }, + title: msg`Settings`, to: "/settings", icon: Gear, minRole: ROLE_ADMIN, @@ -257,7 +240,7 @@ function buildNavItems(manifest: CommandPaletteManifest, userRole: number): NavI }, { id: "security", - title: { kind: "i18n", descriptor: msg`Security Settings` }, + title: msg`Security Settings`, to: "/settings/security", icon: Gear, minRole: ROLE_ADMIN, @@ -279,7 +262,7 @@ function buildNavItems(manifest: CommandPaletteManifest, userRole: number): NavI items.push({ id: `plugin-${pluginId}-${page.path}`, - title: { kind: "api", label }, + title: label, to: `/plugins/${pluginId}${page.path}`, icon: PuzzlePiece, keywords: ["plugin", pluginId], @@ -300,7 +283,8 @@ function filterNavItems( if (!query) return items; const lowerQuery = query.toLowerCase(); return items.filter((item) => { - const titleMatch = navItemTitleText(item.title, translate).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; }); @@ -351,7 +335,7 @@ export function AdminCommandPalette({ manifest }: AdminCommandPaletteProps) { label: msg`Navigation`, items: filteredNavItems.map((item) => ({ id: item.id, - title: navItemTitleText(item.title, t), + title: typeof item.title === "string" ? item.title : t(item.title), to: item.to, params: item.params, icon: , From 04b9baa7dd04b944e284c6cf96e67f4a6668ea21 Mon Sep 17 00:00:00 2001 From: Ophir Bucai Date: Sun, 12 Apr 2026 14:15:57 +0300 Subject: [PATCH 35/48] fix(admin): align built-in widget default title with localized label Built-in palette items omit a hardcoded English `title` on the input; the drag payload uses `t(item.label)` so persisted defaults match the palette. Normalize the Widgets test render import path. --- packages/admin/src/components/Widgets.tsx | 6 +++--- packages/admin/tests/components/Widgets.test.tsx | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/admin/src/components/Widgets.tsx b/packages/admin/src/components/Widgets.tsx index b05731c6c..e35b4631e 100644 --- a/packages/admin/src/components/Widgets.tsx +++ b/packages/admin/src/components/Widgets.tsx @@ -87,13 +87,13 @@ const BUILTIN_WIDGETS: Array<{ id: "palette-content", label: msg`Content Block`, description: msg`Rich text content`, - input: { type: "content", title: "Content Block" }, + input: { type: "content" }, }, { id: "palette-menu", label: msg`Menu`, description: msg`Display a navigation menu`, - input: { type: "menu", title: "Menu" }, + input: { type: "menu" }, }, ]; @@ -376,7 +376,7 @@ export function Widgets() { id={item.id} label={t(item.label)} description={t(item.description)} - widgetInput={item.input} + widgetInput={{ ...item.input, title: t(item.label) }} /> ))} {components.map((comp) => ( diff --git a/packages/admin/tests/components/Widgets.test.tsx b/packages/admin/tests/components/Widgets.test.tsx index fa9a55467..4389035e5 100644 --- a/packages/admin/tests/components/Widgets.test.tsx +++ b/packages/admin/tests/components/Widgets.test.tsx @@ -4,7 +4,7 @@ import * as React from "react"; import { describe, it, expect, vi, beforeEach } from "vitest"; import { Widgets } from "../../src/components/Widgets"; -import { render } from "../utils/render.tsx"; +import { render } from "../utils/render"; vi.mock("../../src/lib/api", async () => { const actual = await vi.importActual("../../src/lib/api"); From 4b9ad17a15c4d9752a84100e5f67a13b1130db6d Mon Sep 17 00:00:00 2001 From: Ophir Bucai Date: Sun, 12 Apr 2026 14:56:08 +0300 Subject: [PATCH 36/48] refactor(admin): complete PortableTextEditor slash i18n and tests Use MessageDescriptor | string for slash title/description; built-ins stay on msg, plugin rows keep API strings and use t(msg`Embed a ${block.label}`) only for the fallback. Resolve menu and filter text with inline typeof checks. Editor tests import the shared render harness without a file extension so useLingui runs under I18nProvider (slash-menu included). --- .../src/components/PortableTextEditor.tsx | 28 +++++++++++-------- .../tests/editor/PortableTextEditor.test.tsx | 3 +- .../admin/tests/editor/slash-menu.test.tsx | 2 +- 3 files changed, 18 insertions(+), 15 deletions(-) diff --git a/packages/admin/src/components/PortableTextEditor.tsx b/packages/admin/src/components/PortableTextEditor.tsx index cbbbf9184..7826fda0b 100644 --- a/packages/admin/src/components/PortableTextEditor.tsx +++ b/packages/admin/src/components/PortableTextEditor.tsx @@ -681,8 +681,9 @@ function convertPTMarks(marks: string[], markDefs: Map; command: (props: { editor: Editor; range: Range }) => void; aliases?: string[]; @@ -954,8 +955,12 @@ function SlashCommandMenu({ >
- {t(item.title)} - {t(item.description)} + + {typeof item.title === "string" ? item.title : t(item.title)} + + + {typeof item.description === "string" ? item.description : t(item.description)} +
)) @@ -1430,15 +1435,12 @@ export function PortableTextEditor({ }, }); - // Add plugin block commands + // Add plugin block commands (API labels/descriptions: plain strings, not msg-wrapped) for (const block of pluginBlocks) { - const labelLower = block.label.toLowerCase(); cmds.push({ id: `plugin-${block.pluginId}-${block.type}`, - title: msg`${block.label}`, - description: block.description - ? msg`${block.description}` - : msg`Embed a ${labelLower}`, + title: block.label, + description: block.description ?? t(msg`Embed a ${block.label}`), icon: resolveIcon(block.icon), aliases: [block.type], category: msg`Embeds`, @@ -1462,10 +1464,12 @@ export function PortableTextEditor({ const titleMatches: SlashCommandItem[] = []; const otherMatches: SlashCommandItem[] = []; for (const item of slashCommands) { - if (t(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 ( - t(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/tests/editor/PortableTextEditor.test.tsx b/packages/admin/tests/editor/PortableTextEditor.test.tsx index 0eb7bdf44..7a2f54d7a 100644 --- a/packages/admin/tests/editor/PortableTextEditor.test.tsx +++ b/packages/admin/tests/editor/PortableTextEditor.test.tsx @@ -10,10 +10,9 @@ import type { Editor } from "@tiptap/react"; import * as React from "react"; import { describe, it, expect, vi } from "vitest"; -import { render } from "../utils/render"; - 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/slash-menu.test.tsx b/packages/admin/tests/editor/slash-menu.test.tsx index f4c53fe3f..314f48059 100644 --- a/packages/admin/tests/editor/slash-menu.test.tsx +++ b/packages/admin/tests/editor/slash-menu.test.tsx @@ -15,7 +15,7 @@ import { describe, it, expect, vi } from "vitest"; import type { PortableTextEditorProps } from "../../src/components/PortableTextEditor"; import { PortableTextEditor } from "../../src/components/PortableTextEditor"; -import { render } from "../utils/render.tsx"; +import { render } from "../utils/render"; // --------------------------------------------------------------------------- // Mocks From 4fd40899e0c28e0b5faefc2f59070c3f99b362d4 Mon Sep 17 00:00:00 2001 From: Ophir Bucai Date: Sun, 12 Apr 2026 15:12:58 +0300 Subject: [PATCH 37/48] refactor(admin): BlockMenu block transforms use msg + lazy t Replace buildBlockTransforms() (macro t) with module-level blockTransforms using msg for transform labels; BlockMenu resolves labels with t() in the Turn into submenu only. Leave main menu copy as literals (lazy-migration PR scope). block-menu tests use shared render; transforms test imports the exported blockTransforms array. --- .../admin/src/components/editor/BlockMenu.tsx | 131 +++++++++--------- .../admin/tests/editor/block-menu.test.tsx | 2 +- .../admin/tests/editor/transforms.test.ts | 4 +- 3 files changed, 66 insertions(+), 71 deletions(-) diff --git a/packages/admin/src/components/editor/BlockMenu.tsx b/packages/admin/src/components/editor/BlockMenu.tsx index ed62936f5..e7115dd07 100644 --- a/packages/admin/src/components/editor/BlockMenu.tsx +++ b/packages/admin/src/components/editor/BlockMenu.tsx @@ -12,7 +12,8 @@ import { Button } from "@cloudflare/kumo"; import { useFloating, offset, flip, shift, autoUpdate } from "@floating-ui/react"; -import { t } from "@lingui/core/macro"; +import type { MessageDescriptor } from "@lingui/core"; +import { msg } from "@lingui/core/macro"; import { useLingui } from "@lingui/react/macro"; import { DotsSixVertical, @@ -41,79 +42,77 @@ import { cn } from "../../lib/utils"; */ interface BlockTransform { id: string; - label: string; + label: MessageDescriptor; icon: PhosphorIcon; transform: (editor: Editor) => void; } -function buildBlockTransforms(): BlockTransform[] { - return [ - { - id: "paragraph", - label: t`Paragraph`, - icon: Paragraph, - transform: (editor) => { - editor.chain().focus().setNode("paragraph").run(); - }, +const blockTransforms: BlockTransform[] = [ + { + id: "paragraph", + label: msg`Paragraph`, + icon: Paragraph, + transform: (editor) => { + editor.chain().focus().setNode("paragraph").run(); }, - { - id: "heading1", - label: t`Heading 1`, - icon: TextHOne, - transform: (editor) => { - editor.chain().focus().setNode("heading", { level: 1 }).run(); - }, + }, + { + id: "heading1", + label: msg`Heading 1`, + icon: TextHOne, + transform: (editor) => { + editor.chain().focus().setNode("heading", { level: 1 }).run(); }, - { - id: "heading2", - label: t`Heading 2`, - icon: TextHTwo, - transform: (editor) => { - editor.chain().focus().setNode("heading", { level: 2 }).run(); - }, + }, + { + id: "heading2", + label: msg`Heading 2`, + icon: TextHTwo, + transform: (editor) => { + editor.chain().focus().setNode("heading", { level: 2 }).run(); }, - { - id: "heading3", - label: t`Heading 3`, - icon: TextHThree, - transform: (editor) => { - editor.chain().focus().setNode("heading", { level: 3 }).run(); - }, + }, + { + id: "heading3", + label: msg`Heading 3`, + icon: TextHThree, + transform: (editor) => { + editor.chain().focus().setNode("heading", { level: 3 }).run(); }, - { - id: "blockquote", - label: t`Quote`, - icon: Quotes, - transform: (editor) => { - editor.chain().focus().toggleBlockquote().run(); - }, + }, + { + id: "blockquote", + label: msg`Quote`, + icon: Quotes, + transform: (editor) => { + editor.chain().focus().toggleBlockquote().run(); }, - { - id: "codeBlock", - label: t`Code Block`, - icon: Code, - transform: (editor) => { - editor.chain().focus().toggleCodeBlock().run(); - }, + }, + { + id: "codeBlock", + label: msg`Code Block`, + icon: Code, + transform: (editor) => { + editor.chain().focus().toggleCodeBlock().run(); }, - { - id: "bulletList", - label: t`Bullet List`, - icon: List, - transform: (editor) => { - editor.chain().focus().toggleBulletList().run(); - }, + }, + { + id: "bulletList", + label: msg`Bullet List`, + icon: List, + transform: (editor) => { + editor.chain().focus().toggleBulletList().run(); }, - { - id: "orderedList", - label: t`Numbered List`, - icon: ListNumbers, - transform: (editor) => { - editor.chain().focus().toggleOrderedList().run(); - }, + }, + { + id: "orderedList", + label: msg`Numbered List`, + icon: ListNumbers, + transform: (editor) => { + editor.chain().focus().toggleOrderedList().run(); }, - ]; -} + }, +]; interface BlockMenuProps { editor: Editor; @@ -129,9 +128,7 @@ interface BlockMenuProps { * Block Menu - floating menu for block-level actions */ export function BlockMenu({ editor, anchorElement, isOpen, onClose }: BlockMenuProps) { - const { i18n } = useLingui(); - const blockTransforms = React.useMemo(() => buildBlockTransforms(), [i18n.locale]); - + const { t } = useLingui(); const [showTransforms, setShowTransforms] = React.useState(false); const menuRef = React.useRef(null); const stableOnClose = useStableCallback(onClose); @@ -265,7 +262,7 @@ export function BlockMenu({ editor, anchorElement, isOpen, onClose }: BlockMenuP onClick={() => handleTransform(transform)} > - {transform.label} + {t(transform.label)} ))} @@ -341,5 +338,5 @@ export function BlockHandle({ onClick, onDragStart, selected }: BlockHandleProps ); } -export { buildBlockTransforms }; +export { blockTransforms }; export type { BlockTransform }; diff --git a/packages/admin/tests/editor/block-menu.test.tsx b/packages/admin/tests/editor/block-menu.test.tsx index ad389951e..cc5ba5564 100644 --- a/packages/admin/tests/editor/block-menu.test.tsx +++ b/packages/admin/tests/editor/block-menu.test.tsx @@ -18,7 +18,7 @@ import { describe, it, expect, vi } from "vitest"; import { BlockMenu } from "../../src/components/editor/BlockMenu"; import { PortableTextEditor } from "../../src/components/PortableTextEditor"; -import { render } from "../utils/render.tsx"; +import { render } from "../utils/render"; // --------------------------------------------------------------------------- // Mocks — same as other editor tests diff --git a/packages/admin/tests/editor/transforms.test.ts b/packages/admin/tests/editor/transforms.test.ts index a820a0195..39d4556bf 100644 --- a/packages/admin/tests/editor/transforms.test.ts +++ b/packages/admin/tests/editor/transforms.test.ts @@ -15,9 +15,7 @@ import { Editor } from "@tiptap/core"; import StarterKit from "@tiptap/starter-kit"; import { describe, it, expect, beforeEach, afterEach } from "vitest"; -import { buildBlockTransforms } from "../../src/components/editor/BlockMenu"; - -const blockTransforms = buildBlockTransforms(); +import { blockTransforms } from "../../src/components/editor/BlockMenu"; describe("Block Transforms", () => { let editor: Editor; From fcd82996e9f7b2d040677fb2266f9ac4437a2e15 Mon Sep 17 00:00:00 2001 From: Ophir Bucai Date: Sun, 12 Apr 2026 15:29:32 +0300 Subject: [PATCH 38/48] refactor(admin): lazy Lingui pattern for AllowedDomainsSettings Use module-level msg for role descriptors; resolve with useLingui in UI. Tests use shared render harness for I18nProvider. --- .../settings/AllowedDomainsSettings.tsx | 48 +++++++++---------- .../settings/AllowedDomainsSettings.test.tsx | 2 +- 2 files changed, 23 insertions(+), 27 deletions(-) diff --git a/packages/admin/src/components/settings/AllowedDomainsSettings.tsx b/packages/admin/src/components/settings/AllowedDomainsSettings.tsx index 9020d6092..56c40dae3 100644 --- a/packages/admin/src/components/settings/AllowedDomainsSettings.tsx +++ b/packages/admin/src/components/settings/AllowedDomainsSettings.tsx @@ -6,7 +6,8 @@ */ import { Button, Dialog, Input, Select, Switch } from "@cloudflare/kumo"; -import { t } from "@lingui/core/macro"; +import type { MessageDescriptor } from "@lingui/core"; +import { msg } from "@lingui/core/macro"; import { useLingui } from "@lingui/react/macro"; import { Globe, @@ -32,30 +33,25 @@ import { type AllowedDomain, } from "../../lib/api"; -function buildRolesConfig() { - const roles = [ - { value: 10, label: t`Subscriber` }, - { value: 20, label: t`Contributor` }, - { value: 30, label: t`Author` }, - { value: 40, label: t`Editor` }, - ]; - const roleLabels: Record = Object.fromEntries( - roles.map((r) => [String(r.value), r.label]), - ); - return { - getRoleLabel: (level: number): string => roleLabels[String(level)] ?? t`Unknown`, - roleLabels, - roles, - }; -} +const MSG_ROLE_UNKNOWN = msg`Unknown`; + +const ROLES: readonly { value: number; label: MessageDescriptor }[] = [ + { value: 10, label: msg`Subscriber` }, + { value: 20, label: msg`Contributor` }, + { value: 30, label: msg`Author` }, + { value: 40, label: msg`Editor` }, +]; export function AllowedDomainsSettings() { - const { i18n } = useLingui(); - const { getRoleLabel, roleLabels, roles } = React.useMemo( - () => buildRolesConfig(), - [i18n.locale], + const { t } = useLingui(); + const roleLabels = React.useMemo( + () => Object.fromEntries(ROLES.map((r) => [String(r.value), t(r.label)])), + [t], + ); + const getRoleLabel = React.useCallback( + (level: number) => roleLabels[String(level)] ?? t(MSG_ROLE_UNKNOWN), + [roleLabels, t], ); - const queryClient = useQueryClient(); const [isAddingDomain, setIsAddingDomain] = React.useState(false); const [editingDomain, setEditingDomain] = React.useState(null); @@ -355,9 +351,9 @@ export function AllowedDomainsSettings() { onValueChange={(v) => v !== null && setNewRole(Number(v))} items={roleLabels} > - {roles.map((role) => ( + {ROLES.map((role) => ( - {role.label} + {t(role.label)} ))} @@ -419,9 +415,9 @@ export function AllowedDomainsSettings() { } items={roleLabels} > - {roles.map((role) => ( + {ROLES.map((role) => ( - {role.label} + {t(role.label)} ))} diff --git a/packages/admin/tests/components/settings/AllowedDomainsSettings.test.tsx b/packages/admin/tests/components/settings/AllowedDomainsSettings.test.tsx index 9187564bd..1dd81a448 100644 --- a/packages/admin/tests/components/settings/AllowedDomainsSettings.test.tsx +++ b/packages/admin/tests/components/settings/AllowedDomainsSettings.test.tsx @@ -4,7 +4,7 @@ import * as React from "react"; import { describe, it, expect, vi, beforeEach } from "vitest"; import { AllowedDomainsSettings } from "../../../src/components/settings/AllowedDomainsSettings"; -import { render } from "../../utils/render.tsx"; +import { render } from "../../utils/render"; vi.mock("@tanstack/react-router", async () => { const actual = await vi.importActual("@tanstack/react-router"); From e266a9118006d615c495f66afd6f143fff3ae729 Mon Sep 17 00:00:00 2001 From: Ophir Bucai Date: Sun, 12 Apr 2026 15:33:47 +0300 Subject: [PATCH 39/48] refactor(admin): lazy Lingui for ApiTokenSettings Module-level msg for expiry options, scope labels (SCOPE_UI), and UI copy; resolve with useLingui. Scope values still come from API_TOKEN_SCOPES in api-tokens. Pass pre-translated expiry map into CreateTokenForm for Select items. Catalog extract for en/de. --- .../components/settings/ApiTokenSettings.tsx | 159 +++++--- packages/admin/src/locales/de/messages.po | 382 +++++++++++------ packages/admin/src/locales/en/messages.po | 384 ++++++++++++------ 3 files changed, 636 insertions(+), 289 deletions(-) diff --git a/packages/admin/src/components/settings/ApiTokenSettings.tsx b/packages/admin/src/components/settings/ApiTokenSettings.tsx index d0110eb0f..b7ed784c9 100644 --- a/packages/admin/src/components/settings/ApiTokenSettings.tsx +++ b/packages/admin/src/components/settings/ApiTokenSettings.tsx @@ -5,7 +5,8 @@ */ import { Button, Checkbox, Input, Loader, Select } from "@cloudflare/kumo"; -import { t } from "@lingui/core/macro"; +import type { MessageDescriptor } from "@lingui/core"; +import { msg } from "@lingui/core/macro"; import { useLingui } from "@lingui/react/macro"; import { ArrowLeft, @@ -25,7 +26,7 @@ import { fetchApiTokens, createApiToken, revokeApiToken, - buildApiTokenScopes, + API_TOKEN_SCOPES, type ApiTokenCreateResult, } from "../../lib/api/api-tokens.js"; import { getMutationError } from "../DialogError.js"; @@ -34,15 +35,35 @@ import { getMutationError } from "../DialogError.js"; // Expiry options // ============================================================================= -function buildExpiryOptions() { - return [ - { value: "none", label: t`No expiry` }, - { value: "7d", label: t`7 days` }, - { value: "30d", label: t`30 days` }, - { value: "90d", label: t`90 days` }, - { value: "365d", label: t`1 year` }, - ] as const; -} +const EXPIRY_OPTIONS = [ + { 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 SCOPE_UI: Record< + (typeof API_TOKEN_SCOPES)[number]["value"], + { label: MessageDescriptor; description: MessageDescriptor } +> = { + "content:read": { label: msg`Content Read`, description: msg`Read content entries` }, + "content:write": { + label: msg`Content Write`, + description: msg`Create, update, delete content`, + }, + "media:read": { label: msg`Media Read`, description: msg`Read media files` }, + "media:write": { + label: msg`Media Write`, + description: msg`Upload and delete media`, + }, + "schema:read": { label: msg`Schema Read`, description: msg`Read collection schemas` }, + "schema:write": { + label: msg`Schema Write`, + description: msg`Modify collection schemas`, + }, + admin: { label: msg`Admin`, description: msg`Full admin access` }, +}; function computeExpiryDate(option: string): string | undefined { if (option === "none") return undefined; @@ -58,6 +79,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); @@ -111,19 +133,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`)}

@@ -135,10 +162,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.`)}

@@ -148,7 +175,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 ? : } @@ -156,14 +183,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`)}

)}
@@ -171,9 +198,9 @@ export function ApiTokenSettings() { variant="ghost" size="sm" onClick={() => setNewToken(null)} - aria-label="Dismiss" + aria-label={t(msg`Dismiss`)} > - Dismiss + {t(msg`Dismiss`)}
@@ -182,6 +209,7 @@ export function ApiTokenSettings() { {/* Create form */} {showCreateForm ? ( @@ -195,7 +223,7 @@ export function ApiTokenSettings() { /> ) : ( )} @@ -207,7 +235,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.`)}
) : (
@@ -221,16 +249,24 @@ 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()}`)}
@@ -241,14 +277,14 @@ export function ApiTokenSettings() { {getMutationError(revokeMutation.error)} )} - Revoke? + {t(msg`Revoke?`)} ) : ( @@ -266,7 +302,7 @@ export function ApiTokenSettings() { variant="ghost" shape="square" onClick={() => setRevokeConfirmId(token.id)} - aria-label="Revoke token" + aria-label={t(msg`Revoke token`)} > @@ -285,17 +321,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) { - const { i18n } = useLingui(); - const apiTokenScopes = React.useMemo(() => buildApiTokenScopes(), [i18n.locale]); - const expiryOptions = React.useMemo(() => buildExpiryOptions(), [i18n.locale]); - +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"); @@ -325,7 +365,7 @@ function CreateTokenForm({ isCreating, error, onSubmit, onCancel }: CreateTokenF return (
-

Create New Token

+

{t(msg`Create New Token`)}

{error && (
@@ -336,51 +376,54 @@ 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`)}
- {apiTokenScopes.map((scope) => ( - - ))} + {API_TOKEN_SCOPES.map((scope) => { + const ui = SCOPE_UI[scope.value]; + return ( + + ); + })}
diff --git a/packages/admin/src/locales/de/messages.po b/packages/admin/src/locales/de/messages.po index d8f78f249..6d1a336d5 100644 --- a/packages/admin/src/locales/de/messages.po +++ b/packages/admin/src/locales/de/messages.po @@ -41,12 +41,11 @@ msgstr "" msgid "90 days" msgstr "" -#: packages/admin/src/components/users/RoleBadge.tsx:34 -#: packages/admin/src/lib/api/api-tokens.ts:50 +#: packages/admin/src/components/settings/ApiTokenSettings.tsx:65 msgid "Admin" msgstr "" -#: packages/admin/src/components/WelcomeModal.tsx:27 +#: packages/admin/src/components/WelcomeModal.tsx:25 msgid "Administrator" msgstr "" @@ -59,16 +58,20 @@ 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:151 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/settings/AllowedDomainsSettings.tsx:39 -#: packages/admin/src/components/users/RoleBadge.tsx:24 -#: packages/admin/src/components/WelcomeModal.tsx:29 +#: packages/admin/src/components/settings/AllowedDomainsSettings.tsx:41 +#: packages/admin/src/components/WelcomeModal.tsx:27 msgid "Author" msgstr "" @@ -77,28 +80,21 @@ msgstr "" msgid "Back to login" msgstr "Zurück zur Anmeldung" -#: packages/admin/src/components/editor/BlockMenu.tsx:101 -#: packages/admin/src/components/PortableTextEditor.tsx:728 -msgid "Bullet List" -msgstr "" - -#: packages/admin/src/components/users/RoleBadge.tsx:21 -msgid "Can create content" +#: packages/admin/src/components/settings/ApiTokenSettings.tsx:146 +msgid "Back to settings" msgstr "" -#: packages/admin/src/components/users/RoleBadge.tsx:31 -msgid "Can manage all content" -msgstr "" - -#: packages/admin/src/components/users/RoleBadge.tsx:26 -msgid "Can publish own content" +#: packages/admin/src/components/editor/BlockMenu.tsx:101 +#: packages/admin/src/components/PortableTextEditor.tsx:729 +msgid "Bullet List" msgstr "" -#: packages/admin/src/components/users/RoleBadge.tsx:16 -msgid "Can view content" +#: packages/admin/src/components/settings/ApiTokenSettings.tsx:297 +#: packages/admin/src/components/settings/ApiTokenSettings.tsx:426 +msgid "Cancel" msgstr "" -#: packages/admin/src/components/AdminCommandPalette.tsx:201 +#: packages/admin/src/components/AdminCommandPalette.tsx:193 msgid "Categories" msgstr "" @@ -114,67 +110,110 @@ 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:758 +#: packages/admin/src/components/PortableTextEditor.tsx:759 msgid "Code Block" msgstr "" -#: packages/admin/src/components/PortableTextEditor.tsx:1425 +#: packages/admin/src/components/settings/ApiTokenSettings.tsx:287 +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 -#: packages/admin/src/components/Widgets.tsx:90 msgid "Content Block" msgstr "" -#: packages/admin/src/lib/api/api-tokens.ts:44 +#: packages/admin/src/components/settings/ApiTokenSettings.tsx:50 msgid "Content Read" msgstr "" -#: packages/admin/src/components/AdminCommandPalette.tsx:193 +#: packages/admin/src/components/AdminCommandPalette.tsx:185 msgid "Content Types" msgstr "" -#: packages/admin/src/lib/api/api-tokens.ts:45 +#: packages/admin/src/components/settings/ApiTokenSettings.tsx:52 msgid "Content Write" msgstr "" -#: packages/admin/src/components/settings/AllowedDomainsSettings.tsx:38 -#: packages/admin/src/components/users/RoleBadge.tsx:19 -#: packages/admin/src/components/WelcomeModal.tsx:30 +#: packages/admin/src/components/settings/AllowedDomainsSettings.tsx:40 +#: packages/admin/src/components/WelcomeModal.tsx:28 msgid "Contributor" msgstr "" -#: packages/admin/src/components/PortableTextEditor.tsx:729 +#: packages/admin/src/components/settings/ApiTokenSettings.tsx:193 +msgid "Copied to clipboard" +msgstr "" + +#: packages/admin/src/components/settings/ApiTokenSettings.tsx:168 +msgid "Copy this token now — it won't be shown again." +msgstr "" + +#: packages/admin/src/components/settings/ApiTokenSettings.tsx:186 +msgid "Copy token" +msgstr "" + +#: packages/admin/src/components/PortableTextEditor.tsx:730 msgid "Create a bullet list" msgstr "" -#: packages/admin/src/components/PortableTextEditor.tsx:739 +#: packages/admin/src/components/PortableTextEditor.tsx:740 msgid "Create a numbered list" msgstr "" +#: packages/admin/src/components/settings/ApiTokenSettings.tsx:368 +msgid "Create New Token" +msgstr "" + #: packages/admin/src/components/Settings.tsx:110 +#: packages/admin/src/components/settings/ApiTokenSettings.tsx:153 msgid "Create personal access tokens for programmatic API access" msgstr "Persönliche Zugangstokens für programmatischen API-Zugriff erstellen" -#: packages/admin/src/lib/api/api-tokens.ts:45 +#: packages/admin/src/components/settings/ApiTokenSettings.tsx:226 +#: packages/admin/src/components/settings/ApiTokenSettings.tsx:423 +msgid "Create Token" +msgstr "" + +#: packages/admin/src/components/settings/ApiTokenSettings.tsx:53 msgid "Create, update, delete content" msgstr "" -#: packages/admin/src/components/ContentTypeEditor.tsx:110 +#. placeholder {0}: new Date(token.createdAt).toLocaleDateString() +#: packages/admin/src/components/settings/ApiTokenSettings.tsx:269 +msgid "Created {0}" +msgstr "" + +#: packages/admin/src/components/ContentTypeEditor.tsx:121 msgid "Created At" msgstr "" -#: packages/admin/src/components/AdminCommandPalette.tsx:139 +#: packages/admin/src/components/settings/ApiTokenSettings.tsx:423 +msgid "Creating..." +msgstr "" + +#: packages/admin/src/components/AdminCommandPalette.tsx:131 msgid "Dashboard" msgstr "" +#: packages/admin/src/components/settings/ApiTokenSettings.tsx:201 +#: packages/admin/src/components/settings/ApiTokenSettings.tsx:203 +msgid "Dismiss" +msgstr "" + #: packages/admin/src/components/Widgets.tsx:95 msgid "Display a navigation menu" msgstr "" -#: packages/admin/src/components/PortableTextEditor.tsx:768 +#: packages/admin/src/components/PortableTextEditor.tsx:769 msgid "Divider" msgstr "" @@ -182,17 +221,20 @@ msgstr "" msgid "Don't have an account? <0>Sign up" msgstr "Noch kein Konto? <0>Registrieren" -#: packages/admin/src/components/ContentTypeEditor.tsx:106 +#: packages/admin/src/components/ContentTypeEditor.tsx:117 msgid "draft, published, or archived" msgstr "" -#: packages/admin/src/components/ContentTypeEditor.tsx:63 +#: packages/admin/src/components/ContentTypeEditor.tsx:69 msgid "Drafts" msgstr "" -#: packages/admin/src/components/settings/AllowedDomainsSettings.tsx:40 -#: packages/admin/src/components/users/RoleBadge.tsx:29 -#: packages/admin/src/components/WelcomeModal.tsx:28 +#: packages/admin/src/components/settings/ApiTokenSettings.tsx:382 +msgid "e.g., CI/CD Pipeline" +msgstr "" + +#: packages/admin/src/components/settings/AllowedDomainsSettings.tsx:42 +#: packages/admin/src/components/WelcomeModal.tsx:26 msgid "Editor" msgstr "" @@ -204,29 +246,34 @@ msgstr "E-Mail" msgid "Email address" msgstr "E-Mail-Adresse" -#. placeholder {0}: block.label.toLowerCase() -#: packages/admin/src/components/PortableTextEditor.tsx:1437 +#. placeholder {0}: block.label +#: packages/admin/src/components/PortableTextEditor.tsx:1443 msgid "Embed a {0}" msgstr "" -#: packages/admin/src/components/PortableTextEditor.tsx:1440 +#: packages/admin/src/components/PortableTextEditor.tsx:1446 msgid "Embeds" msgstr "" -#: packages/admin/src/components/ContentTypeEditor.tsx:79 +#: 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:256 +msgid "Expires {0}" +msgstr "" + +#: packages/admin/src/components/settings/ApiTokenSettings.tsx:409 +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/RoleBadge.tsx:36 -msgid "Full access" -msgstr "" - -#: packages/admin/src/lib/api/api-tokens.ts:50 +#: packages/admin/src/components/settings/ApiTokenSettings.tsx:65 msgid "Full admin access" msgstr "" @@ -234,22 +281,30 @@ msgstr "" 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:698 +#: 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:708 +#: 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:718 +#: packages/admin/src/components/PortableTextEditor.tsx:719 msgid "Heading 3" msgstr "" -#: packages/admin/src/components/ContentTypeEditor.tsx:92 +#: packages/admin/src/components/settings/ApiTokenSettings.tsx:178 +msgid "Hide token" +msgstr "" + +#: packages/admin/src/components/ContentTypeEditor.tsx:103 msgid "ID" msgstr "" @@ -257,31 +312,31 @@ msgstr "" 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:1407 +#: packages/admin/src/components/PortableTextEditor.tsx:1413 msgid "Image" msgstr "" -#: packages/admin/src/components/AdminCommandPalette.tsx:235 +#: packages/admin/src/components/AdminCommandPalette.tsx:227 msgid "Import" msgstr "" -#: packages/admin/src/components/PortableTextEditor.tsx:749 +#: packages/admin/src/components/PortableTextEditor.tsx:750 msgid "Insert a blockquote" msgstr "" -#: packages/admin/src/components/PortableTextEditor.tsx:759 +#: packages/admin/src/components/PortableTextEditor.tsx:760 msgid "Insert a code block" msgstr "" -#: packages/admin/src/components/PortableTextEditor.tsx:769 +#: packages/admin/src/components/PortableTextEditor.tsx:770 msgid "Insert a horizontal rule" msgstr "" -#: packages/admin/src/components/PortableTextEditor.tsx:1422 +#: packages/admin/src/components/PortableTextEditor.tsx:1428 msgid "Insert a reusable section" msgstr "" -#: packages/admin/src/components/PortableTextEditor.tsx:1408 +#: packages/admin/src/components/PortableTextEditor.tsx:1414 msgid "Insert an image" msgstr "" @@ -289,10 +344,19 @@ msgstr "" msgid "Language" msgstr "Sprache" -#: packages/admin/src/components/PortableTextEditor.tsx:699 +#: 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:263 +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" @@ -301,45 +365,64 @@ msgstr "Sprache" msgid "Manage your passkeys and authentication" msgstr "Passkeys und Authentifizierung verwalten" -#: packages/admin/src/components/PortableTextEditor.tsx:1411 +#: packages/admin/src/components/PortableTextEditor.tsx:1417 msgid "Media" msgstr "" -#: packages/admin/src/components/AdminCommandPalette.tsx:162 +#: packages/admin/src/components/AdminCommandPalette.tsx:154 msgid "Media Library" msgstr "" -#: packages/admin/src/lib/api/api-tokens.ts:46 +#: packages/admin/src/components/settings/ApiTokenSettings.tsx:55 msgid "Media Read" msgstr "" -#: packages/admin/src/lib/api/api-tokens.ts:47 +#: packages/admin/src/components/settings/ApiTokenSettings.tsx:57 msgid "Media Write" msgstr "" -#: packages/admin/src/components/PortableTextEditor.tsx:709 +#: packages/admin/src/components/PortableTextEditor.tsx:710 msgid "Medium section heading" msgstr "" #: packages/admin/src/components/Widgets.tsx:94 -#: packages/admin/src/components/Widgets.tsx:96 msgid "Menu" msgstr "" -#: packages/admin/src/components/AdminCommandPalette.tsx:169 +#: packages/admin/src/components/AdminCommandPalette.tsx:161 msgid "Menus" msgstr "" -#: packages/admin/src/lib/api/api-tokens.ts:49 +#: packages/admin/src/components/settings/ApiTokenSettings.tsx:63 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:238 +msgid "No API tokens yet. Create one to get started." +msgstr "" + #: packages/admin/src/components/settings/ApiTokenSettings.tsx:39 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:738 +#: packages/admin/src/components/PortableTextEditor.tsx:739 msgid "Numbered List" msgstr "" @@ -351,64 +434,81 @@ msgstr "Oder fortfahren mit" msgid "Paragraph" msgstr "" -#: packages/admin/src/components/AdminCommandPalette.tsx:227 +#: packages/admin/src/components/AdminCommandPalette.tsx:219 msgid "Plugins" msgstr "" -#: packages/admin/src/components/ContentTypeEditor.tsx:73 +#: packages/admin/src/components/ContentTypeEditor.tsx:79 msgid "Preview" msgstr "" -#: packages/admin/src/components/ContentTypeEditor.tsx:74 +#: packages/admin/src/components/ContentTypeEditor.tsx:80 msgid "Preview content before publishing" msgstr "" -#: packages/admin/src/components/ContentTypeEditor.tsx:122 +#: 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:748 +#: packages/admin/src/components/PortableTextEditor.tsx:749 msgid "Quote" msgstr "" -#: packages/admin/src/lib/api/api-tokens.ts:48 +#: packages/admin/src/components/settings/ApiTokenSettings.tsx:60 msgid "Read collection schemas" msgstr "" -#: packages/admin/src/lib/api/api-tokens.ts:44 +#: packages/admin/src/components/settings/ApiTokenSettings.tsx:50 msgid "Read content entries" msgstr "" -#: packages/admin/src/lib/api/api-tokens.ts:46 +#: packages/admin/src/components/settings/ApiTokenSettings.tsx:55 msgid "Read media files" msgstr "" -#: packages/admin/src/components/ContentTypeEditor.tsx:68 +#: packages/admin/src/components/ContentTypeEditor.tsx:74 msgid "Revisions" msgstr "" -#: packages/admin/src/components/Widgets.tsx:89 -msgid "Rich text content" +#: packages/admin/src/components/settings/ApiTokenSettings.tsx:305 +msgid "Revoke token" msgstr "" -#: packages/admin/src/components/users/RoleBadge.tsx:48 -msgid "Role {role}" +#: packages/admin/src/components/settings/ApiTokenSettings.tsx:280 +msgid "Revoke?" msgstr "" -#: packages/admin/src/components/ContentTypeEditor.tsx:64 +#: packages/admin/src/components/settings/ApiTokenSettings.tsx:287 +msgid "Revoking..." +msgstr "" + +#: packages/admin/src/components/Widgets.tsx:89 +msgid "Rich text content" +msgstr "" + +#: packages/admin/src/components/ContentTypeEditor.tsx:70 msgid "Save content as draft before publishing" msgstr "" -#: packages/admin/src/lib/api/api-tokens.ts:48 +#: packages/admin/src/components/settings/ApiTokenSettings.tsx:60 msgid "Schema Read" msgstr "" -#: packages/admin/src/lib/api/api-tokens.ts:49 +#: packages/admin/src/components/settings/ApiTokenSettings.tsx:62 msgid "Schema Write" msgstr "" -#: packages/admin/src/components/ContentTypeEditor.tsx:78 +#: packages/admin/src/components/settings/ApiTokenSettings.tsx:388 +msgid "Scopes" +msgstr "" + +#. placeholder {0}: token.scopes.join(", ") +#: packages/admin/src/components/settings/ApiTokenSettings.tsx:252 +msgid "Scopes: {0}" +msgstr "" + +#: packages/admin/src/components/ContentTypeEditor.tsx:84 msgid "Search" msgstr "" @@ -416,11 +516,15 @@ msgstr "" msgid "Search engine optimization and verification" msgstr "Suchmaschinenoptimierung und Verifizierung" -#: packages/admin/src/components/PortableTextEditor.tsx:1421 +#: 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:185 +#: packages/admin/src/components/AdminCommandPalette.tsx:177 msgid "Sections" msgstr "" @@ -428,7 +532,7 @@ msgstr "" msgid "Security" msgstr "Sicherheit" -#: packages/admin/src/components/AdminCommandPalette.tsx:251 +#: packages/admin/src/components/AdminCommandPalette.tsx:243 msgid "Security Settings" msgstr "" @@ -448,11 +552,15 @@ msgstr "Wird gesendet..." msgid "SEO" msgstr "SEO" -#: packages/admin/src/components/AdminCommandPalette.tsx:243 +#: 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:178 +msgid "Show token" +msgstr "" + #: packages/admin/src/components/LoginPage.tsx:283 msgid "Sign in to your site" msgstr "Bei Ihrer Website anmelden" @@ -473,11 +581,11 @@ 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:98 +#: packages/admin/src/components/ContentTypeEditor.tsx:109 msgid "Slug" msgstr "" -#: packages/admin/src/components/PortableTextEditor.tsx:719 +#: packages/admin/src/components/PortableTextEditor.tsx:720 msgid "Small section heading" msgstr "" @@ -489,17 +597,16 @@ msgstr "Soziale Netzwerke" msgid "Social media profile links" msgstr "Links zu Social-Media-Profilen" -#: packages/admin/src/components/ContentTypeEditor.tsx:104 +#: packages/admin/src/components/ContentTypeEditor.tsx:115 msgid "Status" msgstr "" -#: packages/admin/src/components/settings/AllowedDomainsSettings.tsx:37 -#: packages/admin/src/components/users/RoleBadge.tsx:14 -#: packages/admin/src/components/WelcomeModal.tsx:31 +#: packages/admin/src/components/settings/AllowedDomainsSettings.tsx:39 +#: packages/admin/src/components/WelcomeModal.tsx:29 msgid "Subscriber" msgstr "" -#: packages/admin/src/components/AdminCommandPalette.tsx:210 +#: packages/admin/src/components/AdminCommandPalette.tsx:202 msgid "Tags" msgstr "" @@ -507,31 +614,44 @@ msgstr "" msgid "The link will expire in 15 minutes." msgstr "Der Link ist 15 Minuten gültig." -#: packages/admin/src/components/ContentTypeEditor.tsx:69 +#: 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:165 +msgid "Token created: {0}" +msgstr "" + +#: packages/admin/src/components/settings/ApiTokenSettings.tsx:379 +msgid "Token Name" +msgstr "" + +#: packages/admin/src/components/ContentTypeEditor.tsx:75 msgid "Track content history" msgstr "" -#: packages/admin/src/components/ContentTypeEditor.tsx:94 +#: packages/admin/src/components/ContentTypeEditor.tsx:105 msgid "Unique identifier (ULID)" msgstr "" -#: packages/admin/src/components/settings/AllowedDomainsSettings.tsx:46 +#: packages/admin/src/components/settings/AllowedDomainsSettings.tsx:36 msgid "Unknown" msgstr "" -#: packages/admin/src/components/users/RoleBadge.tsx:50 -msgid "Unknown role" -msgstr "" - -#: packages/admin/src/components/ContentTypeEditor.tsx:116 +#: packages/admin/src/components/ContentTypeEditor.tsx:127 msgid "Updated At" msgstr "" -#: packages/admin/src/lib/api/api-tokens.ts:47 +#: packages/admin/src/components/settings/ApiTokenSettings.tsx:58 msgid "Upload and delete media" msgstr "" -#: packages/admin/src/components/ContentTypeEditor.tsx:100 +#: packages/admin/src/components/ContentTypeEditor.tsx:111 msgid "URL-friendly identifier" msgstr "" @@ -539,7 +659,7 @@ msgstr "" 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:219 +#: packages/admin/src/components/AdminCommandPalette.tsx:211 msgid "Users" msgstr "" @@ -551,18 +671,50 @@ msgstr "E-Mail-Anbieter-Status anzeigen und Test-E-Mails senden" 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/ContentTypeEditor.tsx:112 +#: 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:118 +#: packages/admin/src/components/ContentTypeEditor.tsx:129 msgid "When the entry was last modified" msgstr "" -#: packages/admin/src/components/ContentTypeEditor.tsx:124 +#: packages/admin/src/components/ContentTypeEditor.tsx:135 msgid "When the entry was published" msgstr "" -#: packages/admin/src/components/AdminCommandPalette.tsx:177 +#: 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 7411de400..1281e566f 100644 --- a/packages/admin/src/locales/en/messages.po +++ b/packages/admin/src/locales/en/messages.po @@ -41,12 +41,11 @@ msgstr "7 days" msgid "90 days" msgstr "90 days" -#: packages/admin/src/components/users/RoleBadge.tsx:34 -#: packages/admin/src/lib/api/api-tokens.ts:50 +#: packages/admin/src/components/settings/ApiTokenSettings.tsx:65 msgid "Admin" msgstr "Admin" -#: packages/admin/src/components/WelcomeModal.tsx:27 +#: packages/admin/src/components/WelcomeModal.tsx:25 msgid "Administrator" msgstr "Administrator" @@ -59,16 +58,20 @@ 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:151 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/settings/AllowedDomainsSettings.tsx:39 -#: packages/admin/src/components/users/RoleBadge.tsx:24 -#: packages/admin/src/components/WelcomeModal.tsx:29 +#: packages/admin/src/components/settings/AllowedDomainsSettings.tsx:41 +#: packages/admin/src/components/WelcomeModal.tsx:27 msgid "Author" msgstr "Author" @@ -77,28 +80,21 @@ msgstr "Author" msgid "Back to login" msgstr "Back to login" +#: packages/admin/src/components/settings/ApiTokenSettings.tsx:146 +msgid "Back to settings" +msgstr "Back to settings" + #: packages/admin/src/components/editor/BlockMenu.tsx:101 -#: packages/admin/src/components/PortableTextEditor.tsx:728 +#: packages/admin/src/components/PortableTextEditor.tsx:729 msgid "Bullet List" msgstr "Bullet List" -#: packages/admin/src/components/users/RoleBadge.tsx:21 -msgid "Can create content" -msgstr "Can create content" +#: packages/admin/src/components/settings/ApiTokenSettings.tsx:297 +#: packages/admin/src/components/settings/ApiTokenSettings.tsx:426 +msgid "Cancel" +msgstr "Cancel" -#: packages/admin/src/components/users/RoleBadge.tsx:31 -msgid "Can manage all content" -msgstr "Can manage all content" - -#: packages/admin/src/components/users/RoleBadge.tsx:26 -msgid "Can publish own content" -msgstr "Can publish own content" - -#: packages/admin/src/components/users/RoleBadge.tsx:16 -msgid "Can view content" -msgstr "Can view content" - -#: packages/admin/src/components/AdminCommandPalette.tsx:201 +#: packages/admin/src/components/AdminCommandPalette.tsx:193 msgid "Categories" msgstr "Categories" @@ -114,67 +110,110 @@ 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:758 +#: packages/admin/src/components/PortableTextEditor.tsx:759 msgid "Code Block" msgstr "Code Block" -#: packages/admin/src/components/PortableTextEditor.tsx:1425 +#: packages/admin/src/components/settings/ApiTokenSettings.tsx:287 +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 -#: packages/admin/src/components/Widgets.tsx:90 msgid "Content Block" msgstr "Content Block" -#: packages/admin/src/lib/api/api-tokens.ts:44 +#: packages/admin/src/components/settings/ApiTokenSettings.tsx:50 msgid "Content Read" msgstr "Content Read" -#: packages/admin/src/components/AdminCommandPalette.tsx:193 +#: packages/admin/src/components/AdminCommandPalette.tsx:185 msgid "Content Types" msgstr "Content Types" -#: packages/admin/src/lib/api/api-tokens.ts:45 +#: packages/admin/src/components/settings/ApiTokenSettings.tsx:52 msgid "Content Write" msgstr "Content Write" -#: packages/admin/src/components/settings/AllowedDomainsSettings.tsx:38 -#: packages/admin/src/components/users/RoleBadge.tsx:19 -#: packages/admin/src/components/WelcomeModal.tsx:30 +#: packages/admin/src/components/settings/AllowedDomainsSettings.tsx:40 +#: packages/admin/src/components/WelcomeModal.tsx:28 msgid "Contributor" msgstr "Contributor" -#: packages/admin/src/components/PortableTextEditor.tsx:729 +#: packages/admin/src/components/settings/ApiTokenSettings.tsx:193 +msgid "Copied to clipboard" +msgstr "Copied to clipboard" + +#: packages/admin/src/components/settings/ApiTokenSettings.tsx:168 +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:186 +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:739 +#: packages/admin/src/components/PortableTextEditor.tsx:740 msgid "Create a numbered list" msgstr "Create a numbered list" +#: packages/admin/src/components/settings/ApiTokenSettings.tsx:368 +msgid "Create New Token" +msgstr "Create New Token" + #: packages/admin/src/components/Settings.tsx:110 +#: packages/admin/src/components/settings/ApiTokenSettings.tsx:153 msgid "Create personal access tokens for programmatic API access" msgstr "Create personal access tokens for programmatic API access" -#: packages/admin/src/lib/api/api-tokens.ts:45 +#: packages/admin/src/components/settings/ApiTokenSettings.tsx:226 +#: packages/admin/src/components/settings/ApiTokenSettings.tsx:423 +msgid "Create Token" +msgstr "Create Token" + +#: packages/admin/src/components/settings/ApiTokenSettings.tsx:53 msgid "Create, update, delete content" msgstr "Create, update, delete content" -#: packages/admin/src/components/ContentTypeEditor.tsx:110 +#. placeholder {0}: new Date(token.createdAt).toLocaleDateString() +#: packages/admin/src/components/settings/ApiTokenSettings.tsx:269 +msgid "Created {0}" +msgstr "Created {0}" + +#: packages/admin/src/components/ContentTypeEditor.tsx:121 msgid "Created At" msgstr "Created At" -#: packages/admin/src/components/AdminCommandPalette.tsx:139 +#: packages/admin/src/components/settings/ApiTokenSettings.tsx:423 +msgid "Creating..." +msgstr "Creating..." + +#: packages/admin/src/components/AdminCommandPalette.tsx:131 msgid "Dashboard" msgstr "Dashboard" +#: packages/admin/src/components/settings/ApiTokenSettings.tsx:201 +#: packages/admin/src/components/settings/ApiTokenSettings.tsx:203 +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:768 +#: packages/admin/src/components/PortableTextEditor.tsx:769 msgid "Divider" msgstr "Divider" @@ -182,17 +221,20 @@ msgstr "Divider" msgid "Don't have an account? <0>Sign up" msgstr "Don't have an account? <0>Sign up" -#: packages/admin/src/components/ContentTypeEditor.tsx:106 +#: packages/admin/src/components/ContentTypeEditor.tsx:117 msgid "draft, published, or archived" msgstr "draft, published, or archived" -#: packages/admin/src/components/ContentTypeEditor.tsx:63 +#: packages/admin/src/components/ContentTypeEditor.tsx:69 msgid "Drafts" msgstr "Drafts" -#: packages/admin/src/components/settings/AllowedDomainsSettings.tsx:40 -#: packages/admin/src/components/users/RoleBadge.tsx:29 -#: packages/admin/src/components/WelcomeModal.tsx:28 +#: packages/admin/src/components/settings/ApiTokenSettings.tsx:382 +msgid "e.g., CI/CD Pipeline" +msgstr "e.g., CI/CD Pipeline" + +#: packages/admin/src/components/settings/AllowedDomainsSettings.tsx:42 +#: packages/admin/src/components/WelcomeModal.tsx:26 msgid "Editor" msgstr "Editor" @@ -204,29 +246,34 @@ msgstr "Email" msgid "Email address" msgstr "Email address" -#. placeholder {0}: block.label.toLowerCase() -#: packages/admin/src/components/PortableTextEditor.tsx:1437 +#. 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:1440 +#: packages/admin/src/components/PortableTextEditor.tsx:1446 msgid "Embeds" msgstr "Embeds" -#: packages/admin/src/components/ContentTypeEditor.tsx:79 +#: 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:256 +msgid "Expires {0}" +msgstr "Expires {0}" + +#: packages/admin/src/components/settings/ApiTokenSettings.tsx:409 +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/RoleBadge.tsx:36 -msgid "Full access" -msgstr "Full access" - -#: packages/admin/src/lib/api/api-tokens.ts:50 +#: packages/admin/src/components/settings/ApiTokenSettings.tsx:65 msgid "Full admin access" msgstr "Full admin access" @@ -234,22 +281,30 @@ msgstr "Full admin access" 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:698 +#: 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:708 +#: 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:718 +#: packages/admin/src/components/PortableTextEditor.tsx:719 msgid "Heading 3" msgstr "Heading 3" -#: packages/admin/src/components/ContentTypeEditor.tsx:92 +#: packages/admin/src/components/settings/ApiTokenSettings.tsx:178 +msgid "Hide token" +msgstr "Hide token" + +#: packages/admin/src/components/ContentTypeEditor.tsx:103 msgid "ID" msgstr "ID" @@ -257,31 +312,31 @@ msgstr "ID" 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:1407 +#: packages/admin/src/components/PortableTextEditor.tsx:1413 msgid "Image" msgstr "Image" -#: packages/admin/src/components/AdminCommandPalette.tsx:235 +#: packages/admin/src/components/AdminCommandPalette.tsx:227 msgid "Import" msgstr "Import" -#: packages/admin/src/components/PortableTextEditor.tsx:749 +#: packages/admin/src/components/PortableTextEditor.tsx:750 msgid "Insert a blockquote" msgstr "Insert a blockquote" -#: packages/admin/src/components/PortableTextEditor.tsx:759 +#: packages/admin/src/components/PortableTextEditor.tsx:760 msgid "Insert a code block" msgstr "Insert a code block" -#: packages/admin/src/components/PortableTextEditor.tsx:769 +#: packages/admin/src/components/PortableTextEditor.tsx:770 msgid "Insert a horizontal rule" msgstr "Insert a horizontal rule" -#: packages/admin/src/components/PortableTextEditor.tsx:1422 +#: packages/admin/src/components/PortableTextEditor.tsx:1428 msgid "Insert a reusable section" msgstr "Insert a reusable section" -#: packages/admin/src/components/PortableTextEditor.tsx:1408 +#: packages/admin/src/components/PortableTextEditor.tsx:1414 msgid "Insert an image" msgstr "Insert an image" @@ -289,10 +344,19 @@ msgstr "Insert an image" msgid "Language" msgstr "Language" -#: packages/admin/src/components/PortableTextEditor.tsx:699 +#: 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:263 +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" @@ -301,45 +365,64 @@ msgstr "Locale" msgid "Manage your passkeys and authentication" msgstr "Manage your passkeys and authentication" -#: packages/admin/src/components/PortableTextEditor.tsx:1411 +#: packages/admin/src/components/PortableTextEditor.tsx:1417 msgid "Media" msgstr "Media" -#: packages/admin/src/components/AdminCommandPalette.tsx:162 +#: packages/admin/src/components/AdminCommandPalette.tsx:154 msgid "Media Library" msgstr "Media Library" -#: packages/admin/src/lib/api/api-tokens.ts:46 +#: packages/admin/src/components/settings/ApiTokenSettings.tsx:55 msgid "Media Read" msgstr "Media Read" -#: packages/admin/src/lib/api/api-tokens.ts:47 +#: packages/admin/src/components/settings/ApiTokenSettings.tsx:57 msgid "Media Write" msgstr "Media Write" -#: packages/admin/src/components/PortableTextEditor.tsx:709 +#: packages/admin/src/components/PortableTextEditor.tsx:710 msgid "Medium section heading" msgstr "Medium section heading" #: packages/admin/src/components/Widgets.tsx:94 -#: packages/admin/src/components/Widgets.tsx:96 msgid "Menu" msgstr "Menu" -#: packages/admin/src/components/AdminCommandPalette.tsx:169 +#: packages/admin/src/components/AdminCommandPalette.tsx:161 msgid "Menus" msgstr "Menus" -#: packages/admin/src/lib/api/api-tokens.ts:49 +#: packages/admin/src/components/settings/ApiTokenSettings.tsx:63 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:238 +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:39 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:738 +#: packages/admin/src/components/PortableTextEditor.tsx:739 msgid "Numbered List" msgstr "Numbered List" @@ -351,64 +434,81 @@ msgstr "Or continue with" msgid "Paragraph" msgstr "Paragraph" -#: packages/admin/src/components/AdminCommandPalette.tsx:227 +#: packages/admin/src/components/AdminCommandPalette.tsx:219 msgid "Plugins" msgstr "Plugins" -#: packages/admin/src/components/ContentTypeEditor.tsx:73 +#: packages/admin/src/components/ContentTypeEditor.tsx:79 msgid "Preview" msgstr "Preview" -#: packages/admin/src/components/ContentTypeEditor.tsx:74 +#: packages/admin/src/components/ContentTypeEditor.tsx:80 msgid "Preview content before publishing" msgstr "Preview content before publishing" -#: packages/admin/src/components/ContentTypeEditor.tsx:122 +#: 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:748 +#: packages/admin/src/components/PortableTextEditor.tsx:749 msgid "Quote" msgstr "Quote" -#: packages/admin/src/lib/api/api-tokens.ts:48 +#: packages/admin/src/components/settings/ApiTokenSettings.tsx:60 msgid "Read collection schemas" msgstr "Read collection schemas" -#: packages/admin/src/lib/api/api-tokens.ts:44 +#: packages/admin/src/components/settings/ApiTokenSettings.tsx:50 msgid "Read content entries" msgstr "Read content entries" -#: packages/admin/src/lib/api/api-tokens.ts:46 +#: packages/admin/src/components/settings/ApiTokenSettings.tsx:55 msgid "Read media files" msgstr "Read media files" -#: packages/admin/src/components/ContentTypeEditor.tsx:68 +#: packages/admin/src/components/ContentTypeEditor.tsx:74 msgid "Revisions" msgstr "Revisions" +#: packages/admin/src/components/settings/ApiTokenSettings.tsx:305 +msgid "Revoke token" +msgstr "Revoke token" + +#: packages/admin/src/components/settings/ApiTokenSettings.tsx:280 +msgid "Revoke?" +msgstr "Revoke?" + +#: packages/admin/src/components/settings/ApiTokenSettings.tsx:287 +msgid "Revoking..." +msgstr "Revoking..." + #: packages/admin/src/components/Widgets.tsx:89 msgid "Rich text content" msgstr "Rich text content" -#: packages/admin/src/components/users/RoleBadge.tsx:48 -msgid "Role {role}" -msgstr "Role {role}" - -#: packages/admin/src/components/ContentTypeEditor.tsx:64 +#: packages/admin/src/components/ContentTypeEditor.tsx:70 msgid "Save content as draft before publishing" msgstr "Save content as draft before publishing" -#: packages/admin/src/lib/api/api-tokens.ts:48 +#: packages/admin/src/components/settings/ApiTokenSettings.tsx:60 msgid "Schema Read" msgstr "Schema Read" -#: packages/admin/src/lib/api/api-tokens.ts:49 +#: packages/admin/src/components/settings/ApiTokenSettings.tsx:62 msgid "Schema Write" msgstr "Schema Write" -#: packages/admin/src/components/ContentTypeEditor.tsx:78 +#: packages/admin/src/components/settings/ApiTokenSettings.tsx:388 +msgid "Scopes" +msgstr "Scopes" + +#. placeholder {0}: token.scopes.join(", ") +#: packages/admin/src/components/settings/ApiTokenSettings.tsx:252 +msgid "Scopes: {0}" +msgstr "Scopes: {0}" + +#: packages/admin/src/components/ContentTypeEditor.tsx:84 msgid "Search" msgstr "Search" @@ -416,11 +516,15 @@ msgstr "Search" msgid "Search engine optimization and verification" msgstr "Search engine optimization and verification" -#: packages/admin/src/components/PortableTextEditor.tsx:1421 +#: 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:185 +#: packages/admin/src/components/AdminCommandPalette.tsx:177 msgid "Sections" msgstr "Sections" @@ -428,7 +532,7 @@ msgstr "Sections" msgid "Security" msgstr "Security" -#: packages/admin/src/components/AdminCommandPalette.tsx:251 +#: packages/admin/src/components/AdminCommandPalette.tsx:243 msgid "Security Settings" msgstr "Security Settings" @@ -448,11 +552,15 @@ msgstr "Sending..." msgid "SEO" msgstr "SEO" -#: packages/admin/src/components/AdminCommandPalette.tsx:243 +#: 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:178 +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" @@ -473,11 +581,11 @@ 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:98 +#: packages/admin/src/components/ContentTypeEditor.tsx:109 msgid "Slug" msgstr "Slug" -#: packages/admin/src/components/PortableTextEditor.tsx:719 +#: packages/admin/src/components/PortableTextEditor.tsx:720 msgid "Small section heading" msgstr "Small section heading" @@ -489,17 +597,16 @@ msgstr "Social Links" msgid "Social media profile links" msgstr "Social media profile links" -#: packages/admin/src/components/ContentTypeEditor.tsx:104 +#: packages/admin/src/components/ContentTypeEditor.tsx:115 msgid "Status" msgstr "Status" -#: packages/admin/src/components/settings/AllowedDomainsSettings.tsx:37 -#: packages/admin/src/components/users/RoleBadge.tsx:14 -#: packages/admin/src/components/WelcomeModal.tsx:31 +#: packages/admin/src/components/settings/AllowedDomainsSettings.tsx:39 +#: packages/admin/src/components/WelcomeModal.tsx:29 msgid "Subscriber" msgstr "Subscriber" -#: packages/admin/src/components/AdminCommandPalette.tsx:210 +#: packages/admin/src/components/AdminCommandPalette.tsx:202 msgid "Tags" msgstr "Tags" @@ -507,31 +614,44 @@ msgstr "Tags" msgid "The link will expire in 15 minutes." msgstr "The link will expire in 15 minutes." -#: packages/admin/src/components/ContentTypeEditor.tsx:69 +#: 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:165 +msgid "Token created: {0}" +msgstr "Token created: {0}" + +#: packages/admin/src/components/settings/ApiTokenSettings.tsx:379 +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:94 +#: packages/admin/src/components/ContentTypeEditor.tsx:105 msgid "Unique identifier (ULID)" msgstr "Unique identifier (ULID)" -#: packages/admin/src/components/settings/AllowedDomainsSettings.tsx:46 +#: packages/admin/src/components/settings/AllowedDomainsSettings.tsx:36 msgid "Unknown" msgstr "Unknown" -#: packages/admin/src/components/users/RoleBadge.tsx:50 -msgid "Unknown role" -msgstr "Unknown role" - -#: packages/admin/src/components/ContentTypeEditor.tsx:116 +#: packages/admin/src/components/ContentTypeEditor.tsx:127 msgid "Updated At" msgstr "Updated At" -#: packages/admin/src/lib/api/api-tokens.ts:47 +#: packages/admin/src/components/settings/ApiTokenSettings.tsx:58 msgid "Upload and delete media" msgstr "Upload and delete media" -#: packages/admin/src/components/ContentTypeEditor.tsx:100 +#: packages/admin/src/components/ContentTypeEditor.tsx:111 msgid "URL-friendly identifier" msgstr "URL-friendly identifier" @@ -539,7 +659,7 @@ msgstr "URL-friendly identifier" msgid "Use your registered passkey to sign in securely." msgstr "Use your registered passkey to sign in securely." -#: packages/admin/src/components/AdminCommandPalette.tsx:219 +#: packages/admin/src/components/AdminCommandPalette.tsx:211 msgid "Users" msgstr "Users" @@ -551,18 +671,50 @@ msgstr "View email provider status and send test emails" 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/ContentTypeEditor.tsx:112 +#: 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:118 +#: 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:124 +#: 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:177 +#: 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" From 1d2239009a38f016373844d95daf6ec6cf011eab Mon Sep 17 00:00:00 2001 From: Ophir Bucai Date: Sun, 12 Apr 2026 16:10:58 +0300 Subject: [PATCH 40/48] refactor(admin): centralize role labels with useRolesConfig - Add roleDefinitions (ROLE_ENTRIES, getRoleConfig) for msg + badge colors - Add useRolesConfig: roleLabels, getRoleLabel, pre-resolved roles rows - Add useAllowedDomainsRolesConfig for default-role selects (cap at Editor) - Wire AllowedDomainsSettings, UserList, UserDetail, InviteUserModal, users route - RoleBadge uses getRoleConfig only; barrel exports useRolesConfig + getRoleConfig - User invite/detail tests use shared I18n render harness - No locale .po in this slice (per migration plan cadence) --- .../settings/AllowedDomainsSettings.tsx | 35 ++------- .../settings/useAllowedDomainsRolesConfig.ts | 34 +++++++++ .../src/components/users/InviteUserModal.tsx | 9 ++- .../admin/src/components/users/RoleBadge.tsx | 75 ++----------------- .../admin/src/components/users/UserDetail.tsx | 9 ++- .../admin/src/components/users/UserList.tsx | 27 ++++--- packages/admin/src/components/users/index.ts | 4 +- .../src/components/users/roleDefinitions.ts | 70 +++++++++++++++++ .../src/components/users/useRolesConfig.ts | 46 ++++++++++++ packages/admin/src/routes/users.tsx | 3 +- .../components/users/InviteUserModal.test.tsx | 2 +- .../components/users/UserDetail.test.tsx | 2 +- 12 files changed, 202 insertions(+), 114 deletions(-) create mode 100644 packages/admin/src/components/settings/useAllowedDomainsRolesConfig.ts create mode 100644 packages/admin/src/components/users/roleDefinitions.ts create mode 100644 packages/admin/src/components/users/useRolesConfig.ts diff --git a/packages/admin/src/components/settings/AllowedDomainsSettings.tsx b/packages/admin/src/components/settings/AllowedDomainsSettings.tsx index 56c40dae3..79fc48255 100644 --- a/packages/admin/src/components/settings/AllowedDomainsSettings.tsx +++ b/packages/admin/src/components/settings/AllowedDomainsSettings.tsx @@ -6,9 +6,6 @@ */ import { Button, Dialog, Input, Select, Switch } from "@cloudflare/kumo"; -import type { MessageDescriptor } from "@lingui/core"; -import { msg } from "@lingui/core/macro"; -import { useLingui } from "@lingui/react/macro"; import { Globe, Plus, @@ -32,26 +29,10 @@ import { fetchManifest, type AllowedDomain, } from "../../lib/api"; - -const MSG_ROLE_UNKNOWN = msg`Unknown`; - -const ROLES: readonly { value: number; label: MessageDescriptor }[] = [ - { value: 10, label: msg`Subscriber` }, - { value: 20, label: msg`Contributor` }, - { value: 30, label: msg`Author` }, - { value: 40, label: msg`Editor` }, -]; +import { useAllowedDomainsRolesConfig } from "./useAllowedDomainsRolesConfig.js"; export function AllowedDomainsSettings() { - const { t } = useLingui(); - const roleLabels = React.useMemo( - () => Object.fromEntries(ROLES.map((r) => [String(r.value), t(r.label)])), - [t], - ); - const getRoleLabel = React.useCallback( - (level: number) => roleLabels[String(level)] ?? t(MSG_ROLE_UNKNOWN), - [roleLabels, t], - ); + const { getRoleLabel, signupRoles, signupRoleItems } = useAllowedDomainsRolesConfig(); const queryClient = useQueryClient(); const [isAddingDomain, setIsAddingDomain] = React.useState(false); const [editingDomain, setEditingDomain] = React.useState(null); @@ -349,11 +330,11 @@ export function AllowedDomainsSettings() { label="Default Role" value={String(newRole)} onValueChange={(v) => v !== null && setNewRole(Number(v))} - items={roleLabels} + items={signupRoleItems} > - {ROLES.map((role) => ( + {signupRoles.map((role) => ( - {t(role.label)} + {role.label} ))} @@ -413,11 +394,11 @@ export function AllowedDomainsSettings() { onValueChange={(v) => v !== null && editingDomain && handleUpdateRole(editingDomain.domain, Number(v)) } - items={roleLabels} + items={signupRoleItems} > - {ROLES.map((role) => ( + {signupRoles.map((role) => ( - {t(role.label)} + {role.label} ))} 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..1def32c12 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,11 @@ 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={Object.fromEntries( + roles.map((r) => [String(r.value), roleLabels[String(r.value)]]), + )} > - {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 5c9ca0e25..cab2776c3 100644 --- a/packages/admin/src/components/users/RoleBadge.tsx +++ b/packages/admin/src/components/users/RoleBadge.tsx @@ -1,61 +1,9 @@ -import { t } from "@lingui/core/macro"; import { useLingui } from "@lingui/react/macro"; -import { useMemo } from "react"; import { cn } from "../../lib/utils"; +import { getRoleConfig } from "./roleDefinitions.js"; -/** Role level to name mapping. */ -export function buildRoleConfig(): Record< - number, - { label: string; color: string; description: string } -> { - return { - 10: { - label: t`Subscriber`, - color: "gray", - description: t`Can view content`, - }, - 20: { - label: t`Contributor`, - color: "blue", - description: t`Can create content`, - }, - 30: { - label: t`Author`, - color: "green", - description: t`Can publish own content`, - }, - 40: { - label: t`Editor`, - color: "purple", - description: t`Can manage all content`, - }, - 50: { - label: t`Admin`, - color: "red", - description: t`Full access`, - }, - }; -} - -/** Get role config, with fallback for unknown roles */ -export function getRoleConfig( - role: number, - map: Record = buildRoleConfig(), -) { - return ( - map[role] ?? { - label: t`Role ${role}`, - color: "gray", - description: t`Unknown role`, - } - ); -} - -/** 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; @@ -73,8 +21,8 @@ export function RoleBadge({ showDescription = false, className, }: RoleBadgeProps) { - const { i18n } = useLingui(); - const config = useMemo(() => getRoleConfig(role, buildRoleConfig()), [i18n.locale, role]); + const { t } = useLingui(); + const config = getRoleConfig(role); const colorClasses: Record = { gray: "bg-gray-100 text-gray-800 dark:bg-gray-800 dark:text-gray-200", @@ -97,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..2b9e17eee 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,11 @@ 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={Object.fromEntries( + roles.map((r) => [String(r.value), roleLabels[String(r.value)]]), + )} > - {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/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/users/InviteUserModal.test.tsx b/packages/admin/tests/components/users/InviteUserModal.test.tsx index a9d99b416..35665a115 100644 --- a/packages/admin/tests/components/users/InviteUserModal.test.tsx +++ b/packages/admin/tests/components/users/InviteUserModal.test.tsx @@ -3,7 +3,7 @@ import * as React from "react"; import { describe, it, expect, vi } from "vitest"; import { InviteUserModal } from "../../../src/components/users/InviteUserModal"; -import { render } from "../../utils/render.tsx"; +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 8aca9a060..865877d9b 100644 --- a/packages/admin/tests/components/users/UserDetail.test.tsx +++ b/packages/admin/tests/components/users/UserDetail.test.tsx @@ -4,7 +4,7 @@ import { describe, it, expect, vi } from "vitest"; import { UserDetail } from "../../../src/components/users/UserDetail"; import type { UserDetail as UserDetailType } from "../../../src/lib/api"; -import { render } from "../../utils/render.tsx"; +import { render } from "../../utils/render"; function makeUser(overrides: Partial = {}): UserDetailType { return { From 382dba0259e0a35fc27a25fd84c9e24069cc187e Mon Sep 17 00:00:00 2001 From: Ophir Bucai Date: Sun, 12 Apr 2026 16:13:42 +0300 Subject: [PATCH 41/48] refactor(admin): API token scopes as values-only in api-tokens Replace API_TOKEN_SCOPES label/description duplicates with API_TOKEN_SCOPE_VALUES + ApiTokenScopeValue; UI copy stays in ApiTokenSettings SCOPE_UI (msg). Re-export from lib/api index. --- .../components/settings/ApiTokenSettings.tsx | 23 +++++++------- packages/admin/src/lib/api/api-tokens.ts | 30 +++++++++---------- packages/admin/src/lib/api/index.ts | 3 +- 3 files changed, 27 insertions(+), 29 deletions(-) diff --git a/packages/admin/src/components/settings/ApiTokenSettings.tsx b/packages/admin/src/components/settings/ApiTokenSettings.tsx index b7ed784c9..34d71b85a 100644 --- a/packages/admin/src/components/settings/ApiTokenSettings.tsx +++ b/packages/admin/src/components/settings/ApiTokenSettings.tsx @@ -26,8 +26,9 @@ import { fetchApiTokens, createApiToken, revokeApiToken, - API_TOKEN_SCOPES, + API_TOKEN_SCOPE_VALUES, type ApiTokenCreateResult, + type ApiTokenScopeValue, } from "../../lib/api/api-tokens.js"; import { getMutationError } from "../DialogError.js"; @@ -44,7 +45,7 @@ const EXPIRY_OPTIONS = [ ] as const; const SCOPE_UI: Record< - (typeof API_TOKEN_SCOPES)[number]["value"], + ApiTokenScopeValue, { label: MessageDescriptor; description: MessageDescriptor } > = { "content:read": { label: msg`Content Read`, description: msg`Read content entries` }, @@ -252,16 +253,12 @@ export function ApiTokenSettings() { {t(msg`Scopes: ${token.scopes.join(", ")}`)} {token.expiresAt && ( - {t( - msg`Expires ${new Date(token.expiresAt).toLocaleDateString()}`, - )} + {t(msg`Expires ${new Date(token.expiresAt).toLocaleDateString()}`)} )} {token.lastUsedAt && ( - {t( - msg`Last used ${new Date(token.lastUsedAt).toLocaleDateString()}`, - )} + {t(msg`Last used ${new Date(token.lastUsedAt).toLocaleDateString()}`)} )}
@@ -387,13 +384,13 @@ function CreateTokenForm({
{t(msg`Scopes`)}
- {API_TOKEN_SCOPES.map((scope) => { - const ui = SCOPE_UI[scope.value]; + {API_TOKEN_SCOPE_VALUES.map((scopeValue) => { + const ui = SCOPE_UI[scopeValue]; return ( -