-
Notifications
You must be signed in to change notification settings - Fork 1.1k
feat(admin): localize plugin admin page labels via the shared Lingui instance #2013
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 2 commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| --- | ||
| "@emdash-cms/admin": minor | ||
| --- | ||
|
|
||
| Plugin admin page labels in the sidebar and command palette are now run through the admin's Lingui instance. Plugins that load a message catalog (with the English label as the message id) get localized navigation, and labels matching one of the admin's own messages (such as "Settings") follow the admin locale automatically. Labels without a catalog entry render unchanged. |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -8,6 +8,7 @@ | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { CommandPalette } from "@cloudflare/kumo"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import type { MessageDescriptor } from "@lingui/core"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { msg } from "@lingui/core/macro"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { useLingui as useLinguiContext } from "@lingui/react"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { useLingui } from "@lingui/react/macro"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| SquaresFour, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -30,6 +31,7 @@ import { useHotkeys } from "react-hotkeys-hook"; | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { apiFetch, type AdminManifest } from "../lib/api/client.js"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { useCurrentUser } from "../lib/api/current-user"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { resolvePluginPageLabel } from "./Sidebar"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** Subset of manifest fields used by the palette (matches `Shell` props shape). */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| type CommandPaletteManifest = { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -124,7 +126,11 @@ async function searchContent(query: string): Promise<SearchResponse> { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return body.data; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| function buildNavItems(manifest: CommandPaletteManifest, userRole: number): NavItem[] { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| function buildNavItems( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| manifest: CommandPaletteManifest, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| userRole: number, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| translateLabel: (id: string) => string, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ): NavItem[] { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const items: NavItem[] = [ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| id: "dashboard", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -253,12 +259,9 @@ function buildNavItems(manifest: CommandPaletteManifest, userRole: number): NavI | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (config.enabled === false) continue; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (config.adminPages && config.adminPages.length > 0) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for (const page of config.adminPages) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const label = | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| page.label || | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| pluginId | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .split("-") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .map((w) => w.charAt(0).toUpperCase() + w.slice(1)) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .join(" "); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Same treatment as the sidebar: declared labels go through the | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // shared i18n instance so plugin catalogs can localize them. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const label = resolvePluginPageLabel(page.label, pluginId, translateLabel); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| items.push({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| id: `plugin-${pluginId}-${page.path}`, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -292,6 +295,11 @@ function filterNavItems( | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export function AdminCommandPalette({ manifest }: AdminCommandPaletteProps) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const { t } = useLingui(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // `_` (not `i18n`) is the nav-items memo dependency: the i18n instance is | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // a stable singleton, while the provider re-binds `_` on every locale or | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // catalog change — exactly the invalidation the plugin labels need. The | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // macro useLingui() omits `_`, so it comes from the runtime hook. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const { _: translateDynamic } = useLinguiContext(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const [open, setOpen] = React.useState(false); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const [query, setQuery] = React.useState(""); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const navigate = useNavigate(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -316,7 +324,10 @@ export function AdminCommandPalette({ manifest }: AdminCommandPaletteProps) { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const isPendingSearch = isWaitingForDebounce || isSearching; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Build navigation items | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const allNavItems = React.useMemo(() => buildNavItems(manifest, userRole), [manifest, userRole]); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const allNavItems = React.useMemo( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| () => buildNavItems(manifest, userRole, translateDynamic), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| [manifest, userRole, translateDynamic], | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [needs fixing] The Either include
Suggested change
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed in 8dbae65 — |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Filter nav items based on query | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const filteredNavItems = React.useMemo( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[needs fixing] The catalog example declares values as
string[](Reports: ["Berichte"]), but Lingui v5 simple messages are strings, not arrays. A developer copying this code would pass the wrong shape toi18n.loadand either fail type checking or get unexpected fallback behavior. Plural/ICU messages are still represented as strings in user-loaded catalogs and compiled to functions by Lingui; they should not be written as arrays here.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed in 8dbae65 — the example now uses plain-string messages (
Reports: "Berichte"), which is the shapei18n.load()expects for uncompiled simple messages.