Skip to content

feat(admin): localize plugin admin page labels via the shared Lingui instance#2013

Open
swissky wants to merge 3 commits into
emdash-cms:mainfrom
swissky:feat/localized-plugin-admin-page-labels
Open

feat(admin): localize plugin admin page labels via the shared Lingui instance#2013
swissky wants to merge 3 commits into
emdash-cms:mainfrom
swissky:feat/localized-plugin-admin-page-labels

Conversation

@swissky

@swissky swissky commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

What does this PR do?

Plugin adminPages labels were rendered verbatim in the sidebar and command palette, so a fully localized admin still showed hard-coded English nav items for plugins — even when the plugin localizes everything inside its pages by merging a Lingui catalog into the shared i18n instance.

This PR runs declared labels through i18n._() at render time in both places, via a new shared resolvePluginPageLabel() helper (deduplicates the previously copy-pasted label logic):

  • Plugins that load a catalog (English label as msgid) get localized navigation.
  • Labels matching one of the admin's own messages (e.g. "Settings") follow the admin locale automatically, so a localized admin no longer shows stray English nav items.
  • Labels with no catalog entry anywhere render unchanged (i18n._() falls back to the msgid) — existing plugins are unaffected.
  • The prettified-plugin-id fallback (no label declared) is not translated, since it's derived from the package name rather than authored English.

Reactivity: the sidebar re-renders through useLingui()'s context on every locale/catalog change; the palette's nav-item memo depends on the context's _ binding, which the provider recreates on each change.

The "React admin" plugin guide now documents the contract and shows the catalog-merge pattern (including re-merging on the locale switcher's catalog replacement).

Discussion: #2012

Type of change

  • Bug fix
  • Feature (requires maintainer-approved Discussion)
  • Refactor (no behavior change)
  • Translation
  • Documentation
  • Performance improvement
  • Tests
  • Chore (dependencies, CI, tooling)

Checklist

AI-generated code disclosure

  • This PR includes AI-generated code — model/tool: Cursor + Fable

Screenshots / test output

New unit tests for resolvePluginPageLabel (translated label / untranslated fallback / plugin-id prettification); full @emdash-cms/admin suite passes locally:

Test Files  89 passed (89)
     Tests  1103 passed (1103)

Verified end to end against a real plugin that merges its own Lingui catalog: sidebar and ⌘K palette entries switch language together with the rest of the admin.

swissky added 2 commits July 13, 2026 13:23
…instance

Plugin adminPages labels were rendered verbatim in the sidebar and command
palette, so a fully localized admin still showed hard-coded English nav
items for plugins. Run declared labels through i18n._() at render time:
plugins that merge a Lingui catalog into the shared instance (English label
as msgid) get localized navigation, and labels without a catalog entry fall
back to the literal string, keeping existing plugins unchanged.
@changeset-bot

changeset-bot Bot commented Jul 13, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 4abaaf3

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 16 packages
Name Type
@emdash-cms/admin Minor
emdash Minor
@emdash-cms/cloudflare Minor
@emdash-cms/sandbox-workerd Patch
@emdash-cms/fixture-perf-site Patch
@emdash-cms/perf-demo-site Patch
@emdash-cms/cache-demo-site Patch
@emdash-cms/do-demo-site Patch
@emdash-cms/do-solo-demo-site Patch
@emdash-cms/auth Minor
@emdash-cms/blocks Minor
@emdash-cms/gutenberg-to-portable-text Minor
@emdash-cms/x402 Minor
create-emdash Minor
@emdash-cms/auth-atproto Patch
@emdash-cms/plugin-embeds Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@swissky swissky added the bot:review Trigger an emdashbot code review on this PR label Jul 13, 2026
@pkg-pr-new

pkg-pr-new Bot commented Jul 13, 2026

Copy link
Copy Markdown

Open in StackBlitz

@emdash-cms/admin

npm i https://pkg.pr.new/@emdash-cms/admin@2013

@emdash-cms/auth

npm i https://pkg.pr.new/@emdash-cms/auth@2013

@emdash-cms/auth-atproto

npm i https://pkg.pr.new/@emdash-cms/auth-atproto@2013

@emdash-cms/blocks

npm i https://pkg.pr.new/@emdash-cms/blocks@2013

@emdash-cms/cloudflare

npm i https://pkg.pr.new/@emdash-cms/cloudflare@2013

@emdash-cms/contentful-to-portable-text

npm i https://pkg.pr.new/@emdash-cms/contentful-to-portable-text@2013

emdash

npm i https://pkg.pr.new/emdash@2013

create-emdash

npm i https://pkg.pr.new/create-emdash@2013

@emdash-cms/gutenberg-to-portable-text

npm i https://pkg.pr.new/@emdash-cms/gutenberg-to-portable-text@2013

@emdash-cms/plugin-cli

npm i https://pkg.pr.new/@emdash-cms/plugin-cli@2013

@emdash-cms/plugin-types

npm i https://pkg.pr.new/@emdash-cms/plugin-types@2013

@emdash-cms/registry-client

npm i https://pkg.pr.new/@emdash-cms/registry-client@2013

@emdash-cms/registry-lexicons

npm i https://pkg.pr.new/@emdash-cms/registry-lexicons@2013

@emdash-cms/sandbox-workerd

npm i https://pkg.pr.new/@emdash-cms/sandbox-workerd@2013

@emdash-cms/x402

npm i https://pkg.pr.new/@emdash-cms/x402@2013

@emdash-cms/plugin-ai-moderation

npm i https://pkg.pr.new/@emdash-cms/plugin-ai-moderation@2013

@emdash-cms/plugin-atproto

npm i https://pkg.pr.new/@emdash-cms/plugin-atproto@2013

@emdash-cms/plugin-audit-log

npm i https://pkg.pr.new/@emdash-cms/plugin-audit-log@2013

@emdash-cms/plugin-color

npm i https://pkg.pr.new/@emdash-cms/plugin-color@2013

@emdash-cms/plugin-embeds

npm i https://pkg.pr.new/@emdash-cms/plugin-embeds@2013

@emdash-cms/plugin-field-kit

npm i https://pkg.pr.new/@emdash-cms/plugin-field-kit@2013

@emdash-cms/plugin-forms

npm i https://pkg.pr.new/@emdash-cms/plugin-forms@2013

@emdash-cms/plugin-webhook-notifier

npm i https://pkg.pr.new/@emdash-cms/plugin-webhook-notifier@2013

commit: 8dbae65

@emdashbot emdashbot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the right change for the issue described in Discussion #2012. Running plugin adminPages labels through the shared Lingui instance fits EmDash’s Lingui-first admin UI conventions, and extracting resolvePluginPageLabel removes the duplicated label logic between the sidebar and command palette. The changeset is user-facing and correctly scopes the minor bump to @emdash-cms/admin.

What I checked:

  • The diff, full changed files, and existing usages of useLingui / i18n in the admin package.
  • Whether the macro useLingui() exposes i18n (it does, confirmed by existing code in BackupSettings.tsx).
  • Lingui v5 catalog/message shape expectations.

Headline conclusion: the implementation is clean, but I have two concerns. The command palette’s useMemo relies on _ reference identity for locale reactivity, which is not a guaranteed Lingui behavior and can leave plugin labels stale after a locale switch. The docs example also uses an incorrect message-catalog value shape (string[] instead of string for simple messages), so copy-pasted plugin code would fail.

Both are fixable with small, targeted edits.

Comment on lines +327 to +329
const allNavItems = React.useMemo(
() => buildNavItems(manifest, userRole, translateDynamic),
[manifest, userRole, translateDynamic],

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[needs fixing] The allNavItems memo depends on translateDynamic (a.k.a. _) for reactivity, but _ is a method on the stable i18n singleton and does not have a documented/provable identity change when the locale or catalog changes. React will re-render this component when I18nProvider updates, but useMemo will not recompute unless a dependency changes, so plugin labels can stay in the previous language after a locale switch.

Either include i18n.locale (and, for catalog merges that happen without a locale change, i18n.messages) in the dependency array, or translate the labels in the render/filter path where the non-memoized t macro already re-renders.

Suggested change
const allNavItems = React.useMemo(
() => buildNavItems(manifest, userRole, translateDynamic),
[manifest, userRole, translateDynamic],
const { _: translateDynamic, i18n } = useLinguiContext();
const [open, setOpen] = React.useState(false);
const [query, setQuery] = React.useState("");
const navigate = useNavigate();
// Debounce the search query to avoid flickering on every keystroke
const debouncedQuery = useDebouncedValue(query, SEARCH_DEBOUNCE_MS);
const { data: user } = useCurrentUser();
const userRole = user?.role ?? 0;
// Search content when debounced query is long enough
const { data: searchResults, isFetching: isSearching } = useQuery({
queryKey: ["command-palette-search", debouncedQuery],
queryFn: () => searchContent(debouncedQuery),
enabled: debouncedQuery.length >= 2,
staleTime: 30 * 1000,
});
// Show loading while waiting for debounce or API response
const isWaitingForDebounce = query.length >= 2 && query !== debouncedQuery;
const isPendingSearch = isWaitingForDebounce || isSearching;
// Build navigation items
const allNavItems = React.useMemo(
() => buildNavItems(manifest, userRole, translateDynamic),
[manifest, userRole, translateDynamic, i18n.locale],
);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 8dbae65i18n.locale is now in the dependency array as the documented locale-switch signal, with the _ rebind still covering catalog merges that arrive without a locale change (plugins load their catalogs asynchronously). Kept the memo rather than moving translation into the filter path so the labels aren't re-translated on every keystroke.


// Merge the plugin's compiled catalog into the admin's i18n instance.
// "Reports" now renders as "Berichte" when the admin locale is German.
const catalogs: Record<string, Record<string, string[]>> = {

Copy link
Copy Markdown
Contributor

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 to i18n.load and 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.

Suggested change
const catalogs: Record<string, Record<string, string[]>> = {
const catalogs: Record<string, Record<string, string>> = {
de: { Reports: "Berichte", Settings: "Einstellungen" },
};

Copy link
Copy Markdown
Contributor Author

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 shape i18n.load() expects for uncompiled simple messages.

…shape

Review follow-ups: the palette memo now also depends on i18n.locale (the
documented locale-switch signal) rather than relying solely on the context
_ rebind, and the docs example uses plain-string catalog messages instead
of the compiled-token array shape.
@github-actions github-actions Bot added review/needs-rereview Author pushed changes since the last review and removed review/needs-review No maintainer or bot review yet labels Jul 13, 2026
@swissky swissky added bot:review Trigger an emdashbot code review on this PR and removed bot:review Trigger an emdashbot code review on this PR labels Jul 13, 2026

@emdashbot emdashbot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the right follow-up for Discussion #2012. Running plugin adminPages labels through the shared Lingui instance fits EmDash’s Lingui-first admin conventions, and extracting resolvePluginPageLabel removes the duplicated sidebar/palette label logic.

I re-read the diff and the full changed files. Both concerns from the previous review are addressed:

  • Command palette reactivity: the useMemo building nav items now depends on both translateDynamic (_ from @lingui/react) and i18n.locale, so locale switches invalidate the memo explicitly even if _ reference identity were to stay stable.
  • Docs catalog shape: the new src/admin.tsx example uses Record<string, Record<string, string>> with plain string message values, matching Lingui v5’s catalog expectations.

The implementation is clean: the helper is unit-tested for translated / fallback / prettified-id cases, the changeset scopes the minor bump to @emdash-cms/admin, and the comments explain the reactivity contract clearly. I found no remaining logic bugs, regression risks, or AGENTS.md convention violations.

@github-actions github-actions Bot added review/approved Approved; no new commits since and removed review/needs-rereview Author pushed changes since the last review labels Jul 13, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/admin area/docs bot:review Trigger an emdashbot code review on this PR review/approved Approved; no new commits since size/M

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant