Skip to content

feat(admin): add i18n with Lingui#234

Open
ophirbucai wants to merge 82 commits intoemdash-cms:mainfrom
ophirbucai:feat/admin-i18n-lingui
Open

feat(admin): add i18n with Lingui#234
ophirbucai wants to merge 82 commits intoemdash-cms:mainfrom
ophirbucai:feat/admin-i18n-lingui

Conversation

@ophirbucai
Copy link
Copy Markdown

@ophirbucai ophirbucai commented Apr 4, 2026

What does this PR do?

Adds Lingui i18n infrastructure to the admin UI. Zero user-visible change — this is plumbing for future locale support.

  • Lingui setup with @lingui/core, @lingui/react, @lingui/macro
  • I18nProvider wired into the admin shell via PluginRegistry
  • English .po catalog extracted (1260 strings across 80+ components)
  • All admin components wrapped with t, <Trans>, <Plural> macros
  • Aria-label attributes wrapped for accessibility i18n
  • Catalogs pre-compiled to .mjs at build time via lingui compile
  • Dev mode imports .po directly via Vite plugin for instant feedback

Related

Design decisions

  • Extracted English only — the language selector is gated behind SUPPORTED_LOCALES.length > 1, invisible until a second locale ships.
  • Role names left untouched, whether to translate Admin/Editor/Author/Contributor is a separate decision.
  • All @lingui/* versions managed via pnpm catalog.
  • Pre-compiled catalogslingui compile runs at admin build time. Consuming sites import .mjs, no Lingui config or Vite plugin needed.
  • Automatic macro compilation — tsdown plugin compiles Lingui macros at build time for the published npm package. The emdash integration injects a Vite plugin in dev mode for monorepo HMR. Consumers need zero Babel config.
  • No changeset included — no user-visible behavior change. Happy to add one if needed.

Type of change

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

Checklist

AI-generated code disclosure

  • This PR includes AI-generated code

Screenshots / test output

  • pnpm typecheck — all packages pass
  • pnpm --filter @emdash-cms/admin test — 709 component tests pass
  • pnpm --filter emdash test — 2114 core tests pass
  • pnpm test:e2e — 213 e2e tests pass (all 8 shards)
  • pnpm lint:json — 0 diagnostics
  • lingui extract — catalog in sync

@changeset-bot
Copy link
Copy Markdown

changeset-bot bot commented Apr 4, 2026

⚠️ No Changeset found

Latest commit: 827623e

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

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

@ophirbucai ophirbucai force-pushed the feat/admin-i18n-lingui branch 3 times, most recently from b4b7a60 to ffa976f Compare April 4, 2026 12:00
@ophirbucai ophirbucai marked this pull request as ready for review April 4, 2026 15:26
Copilot AI review requested due to automatic review settings April 4, 2026 15:26
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds Lingui-based internationalization plumbing to the EmDash admin UI, including server-side locale resolution and client-side catalog activation, while wrapping existing admin strings with Lingui macros for future translation support.

Changes:

  • Introduces Lingui configuration, dependencies, and an admin locale API (resolveLocale, useLocale, supported locales config).
  • Wires Lingui into the admin shell (Astro route → plugin registry wrapper → AdminApp with I18nProvider).
  • Wraps/admin UI strings across routes and components using t, <Trans>, and <Plural> macros.

Reviewed changes

Copilot reviewed 82 out of 84 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
skills/adding-admin-locale/SKILL.md Adds internal documentation for adding locales and using Lingui macros.
pnpm-workspace.yaml Adds Lingui packages to the pnpm catalog.
packages/core/src/astro/routes/PluginRegistry.tsx Passes locale/messages props through to AdminApp.
packages/core/src/astro/routes/admin.astro Resolves locale server-side and imports the locale catalog for the admin shell.
packages/admin/tsdown.config.ts Expands build entrypoints to include locales index.
packages/admin/tsconfig.json Adds vite/client types for import.meta.env typing.
packages/admin/src/routes/users.tsx Wraps user management route strings with Lingui macros.
packages/admin/src/routes/bylines.tsx Wraps bylines route strings and labels with Lingui macros.
packages/admin/src/locales/useLocale.ts Adds a hook for client-side locale switching + cookie persistence + dynamic catalog import.
packages/admin/src/locales/index.ts Adds locale-related barrel exports.
packages/admin/src/locales/config.ts Defines supported locales and server-side locale resolution from cookie/Accept-Language.
packages/admin/src/index.ts Re-exports locale APIs from the package root.
packages/admin/src/components/WelcomeModal.tsx Wraps welcome modal strings and role labels with Lingui macros.
packages/admin/src/components/users/UserList.tsx Wraps user list UI strings with Lingui macros.
packages/admin/src/components/users/UserDetail.tsx Wraps user detail panel strings with Lingui macros.
packages/admin/src/components/users/InviteUserModal.tsx Wraps invite modal strings with Lingui macros.
packages/admin/src/components/ThemeToggle.tsx Wraps theme toggle labels/tooltips with Lingui macros.
packages/admin/src/components/ThemeMarketplaceDetail.tsx Wraps theme marketplace detail strings with Lingui macros.
packages/admin/src/components/ThemeMarketplaceBrowse.tsx Wraps theme marketplace browse strings with Lingui macros.
packages/admin/src/components/TaxonomySidebar.tsx Wraps taxonomy sidebar strings and aria-labels with Lingui macros.
packages/admin/src/components/Sidebar.tsx Wraps sidebar navigation group/item labels with Lingui macros.
packages/admin/src/components/SetupWizard.tsx Wraps setup wizard strings and introduces pluralization with Lingui macros.
packages/admin/src/components/settings/SocialSettings.tsx Wraps social settings strings with Lingui macros.
packages/admin/src/components/settings/SeoSettings.tsx Wraps SEO settings strings with Lingui macros.
packages/admin/src/components/settings/SecuritySettings.tsx Wraps security settings strings with Lingui macros.
packages/admin/src/components/settings/PasskeyItem.tsx Wraps passkey item strings with Lingui macros.
packages/admin/src/components/settings/GeneralSettings.tsx Wraps general settings strings with Lingui macros.
packages/admin/src/components/settings/EmailSettings.tsx Wraps email settings strings with Lingui macros.
packages/admin/src/components/settings/ApiTokenSettings.tsx Wraps API token settings strings with Lingui macros.
packages/admin/src/components/Settings.tsx Adds admin-language selector UI (gated) and wraps strings with Lingui macros.
packages/admin/src/components/SeoPanel.tsx Wraps SEO panel labels/descriptions with Lingui macros.
packages/admin/src/components/Sections.tsx Wraps sections UI strings with Lingui macros.
packages/admin/src/components/SectionPickerModal.tsx Wraps section picker modal strings with Lingui macros.
packages/admin/src/components/SectionEditor.tsx Wraps section editor strings with Lingui macros.
packages/admin/src/components/SaveButton.tsx Wraps save button labels with Lingui macros.
packages/admin/src/components/SandboxedPluginWidget.tsx Wraps sandboxed widget empty/error strings with Lingui macros.
packages/admin/src/components/SandboxedPluginPage.tsx Wraps sandboxed page error heading/message with Lingui macros.
packages/admin/src/components/RevisionHistory.tsx Wraps revision history UI strings (incl. conditional plural forms) with Lingui macros.
packages/admin/src/components/PluginFieldErrorBoundary.tsx Wraps plugin widget error fallback strings with Lingui macros.
packages/admin/src/components/MenuList.tsx Wraps menu list UI strings with Lingui macros.
packages/admin/src/components/MediaPickerModal.tsx Wraps media picker strings and pluralizes item count via Lingui macros.
packages/admin/src/components/MediaLibrary.tsx Wraps media library strings and introduces Lingui plural helpers for upload messaging.
packages/admin/src/components/MediaDetailPanel.tsx Wraps media detail panel labels/buttons with Lingui macros.
packages/admin/src/components/MarketplaceBrowse.tsx Wraps plugin marketplace browse strings and plural forms with Lingui macros.
packages/admin/src/components/LoginPage.tsx Wraps login strings with Lingui macros and adds (gated) locale selector.
packages/admin/src/components/LocaleSwitcher.tsx Wraps locale switcher labels/tooltips with Lingui macros.
packages/admin/src/components/Header.tsx Wraps header menu items with Lingui macros.
packages/admin/src/components/editor/PluginBlockNode.tsx Wraps editor plugin block node labels/tooltips with Lingui macros.
packages/admin/src/components/editor/ImageNode.tsx Wraps image node editor labels/tooltips with Lingui macros.
packages/admin/src/components/editor/DocumentOutline.tsx Wraps document outline strings with Lingui macros.
packages/admin/src/components/editor/BlockMenu.tsx Wraps block menu strings with Lingui macros and refactors transform labels.
packages/admin/src/components/DeviceAuthorizePage.tsx Wraps device authorization strings with Lingui macros.
packages/admin/src/components/Dashboard.tsx Wraps dashboard UI strings and count labels with Lingui macros.
packages/admin/src/components/ContentTypeList.tsx Wraps content type list strings with Lingui macros.
packages/admin/src/components/ContentPickerModal.tsx Wraps content picker modal strings with Lingui macros.
packages/admin/src/components/ConfirmDialog.tsx Wraps confirm dialog cancel label with Lingui macros.
packages/admin/src/components/comments/CommentDetail.tsx Wraps comment detail panel strings with Lingui macros.
packages/admin/src/components/BlockKitFieldWidget.tsx Wraps block-kit widget fallback/select labels with Lingui macros.
packages/admin/src/components/auth/PasskeyRegistration.tsx Wraps passkey registration strings with Lingui macros and adds default button text handling.
packages/admin/src/components/auth/PasskeyLogin.tsx Wraps passkey login strings with Lingui macros and adds default button text handling.
packages/admin/src/components/AdminCommandPalette.tsx Wraps command palette strings and navigation labels with Lingui macros.
packages/admin/src/App.tsx Adds I18nProvider and activates Lingui with server-provided locale/messages.
packages/admin/package.json Adds locale exports and Lingui scripts/dependencies.
lingui.config.ts Adds Lingui extraction configuration (English-only catalog).
demos/simple/package.json Adds Lingui tooling dependencies for the demo app.
demos/simple/astro.config.mjs Configures Babel macro plugin + Vite Lingui plugin in the demo Astro pipeline.
Files not reviewed (1)
  • pnpm-lock.yaml: Language not supported
Comments suppressed due to low confidence (1)

packages/admin/tsdown.config.ts:8

  • The published admin package won’t be able to load locale catalogs: useLocale() dynamically imports ./${code}/messages.po relative to the built dist/locales/* files, but the build config only emits JS/DTs and doesn’t copy .po catalogs into dist. This will make locale switching (and any runtime/SSR catalog import that targets dist) fail in consumer projects. Consider copying src/locales/**/messages.po into dist/locales/**/messages.po as part of the build (or changing the runtime import strategy) so the catalogs exist alongside the compiled locale modules.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 20 to 23
"./locales/*": "./src/locales/*"
},
"scripts": {
"build": "tsdown && npx @tailwindcss/cli -i src/styles.css -o dist/styles.css --minify",
Copy link

Copilot AI Apr 4, 2026

Choose a reason for hiding this comment

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

exports["./locales/*"] points at ./src/locales/*, but this package’s files list only publishes dist, so consumers won’t get src/locales/** (including messages.po). Additionally, the runtime locale loader in dist/locales/useLocale.js uses relative imports, so catalogs need to be present in dist anyway. Fix by either (a) publishing the .po catalogs (and any needed source) via files, or preferably (b) copying catalogs into dist/locales/** and exporting ./locales/* from dist.

Suggested change
"./locales/*": "./src/locales/*"
},
"scripts": {
"build": "tsdown && npx @tailwindcss/cli -i src/styles.css -o dist/styles.css --minify",
"./locales/*": "./dist/locales/*"
},
"scripts": {
"build": "tsdown && npx @tailwindcss/cli -i src/styles.css -o dist/styles.css --minify && node -e \"const fs=require('fs'); fs.mkdirSync('dist/locales', { recursive: true }); fs.cpSync('src/locales', 'dist/locales', { recursive: true });\"",

Copilot uses AI. Check for mistakes.
Comment on lines +15 to +18
import { resolveLocale } from "@emdash-cms/admin/locales";
const resolvedLocale = resolveLocale(Astro.request);
const { messages } = await import(`@emdash-cms/admin/locales/${resolvedLocale}/messages.po`);
Copy link

Copilot AI Apr 4, 2026

Choose a reason for hiding this comment

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

admin.astro now imports .po catalogs directly. That requires @lingui/vite-plugin to be present in the host app’s Vite pipeline for both dev and production builds; otherwise the .po import won’t be understood and the admin route build will fail. Since the EmDash integration’s Vite config currently only adds the virtual-modules plugin, consider either adding the Lingui Vite plugin at the integration level or switching to importing precompiled JS catalogs (so host apps don’t need extra Vite config).

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

@ascorbic
The Lingui Vite plugin is currently configured in demos/simple only. For other demos, templates, and published sites, admin.astro's .po import will fail without it. We could inject it from the EmDash integration in astro:config:setup, but lingui() eagerly searches for a lingui.config.ts.

Which direction do you see this resolved? We can ship a default config from core, or resolve the catalog paths programmatically, unless you have other idea in mind?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Is there any downside to injecting it in the integration? That's generally the approach we'd take

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

When someone installs emdash from npm and creates a site, they won't have a lingui.config.ts in their project. The integration would call lingui(), which calls getConfig(), which searches up from cwd and finds nothing.

In the monorepo (all demos, templates, CI), the root lingui.config.ts is always reachable. So it's only a problem for published consumers.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Will this then cause it to fail? I wonder if we can conditionally add the integration if the config exists, or add the config ourselves. We could at least add it to the default templates. Maybe it should be a config option for the integration.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Here's a working implementation: ophirbucai/emdash@feat/admin-i18n-lingui...spike/lingui-integration-injection
The lingui.config.ts is distributed through the core build - tsdown.config.ts

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

That's looking good in part. However instead of auto-injecting the Vite plugin into the user's site, I'd pre-compile the .po files to JS. I think this could be done with he CLI as part of the admin build?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

That makes absolute sense - I'll see that it works, all ping you once I am confident in the solution

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

All done - consuming sites use the pre-built messages.mjs, which runs through lingui compile during build, no Lingui config or Vite plugin needed for consumers.
Dev mode imports .po via the Vite plugin so changes are reflected immediately during development.

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new bot commented Apr 4, 2026

Open in StackBlitz

@emdash-cms/admin

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

@emdash-cms/auth

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

@emdash-cms/blocks

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

@emdash-cms/cloudflare

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

emdash

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

create-emdash

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

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

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

@emdash-cms/x402

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

@emdash-cms/plugin-ai-moderation

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

@emdash-cms/plugin-atproto

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

@emdash-cms/plugin-audit-log

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

@emdash-cms/plugin-color

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

@emdash-cms/plugin-embeds

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

@emdash-cms/plugin-forms

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

@emdash-cms/plugin-webhook-notifier

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

commit: de26486

@ophirbucai ophirbucai force-pushed the feat/admin-i18n-lingui branch from 146b661 to 0c53403 Compare April 4, 2026 20:29
@ophirbucai
Copy link
Copy Markdown
Author

@ascorbic Hi Matt, the Lingui macros compile to useLingui() which requires I18nProvider in the React tree. To make component tests work, I've:

Created a shared test render utility (tests/utils/render.tsx) that wraps with I18nProvider
Updated 36 test files with a single import change:

- import { render } from "vitest-browser-react";
+ import { render } from "../utils/render.js";

No test logic changes — just the import path. The render utility initializes Lingui with empty English messages so source strings render as-is.

@ophirbucai
Copy link
Copy Markdown
Author

All 707 admin component tests passing locally, including the 36 test files updated to use the I18nProvider wrapper. Core tests (2,095) also pass.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 5, 2026

Overlapping PRs

This PR modifies files that are also changed by other open PRs:

This may cause merge conflicts or duplicated work. A maintainer will coordinate.

@ascorbic
Copy link
Copy Markdown
Collaborator

ascorbic commented Apr 5, 2026

This is looking great so far. I just need to get some clarity on how the setup will need to be handled for sites. Generally I want to avoid the need to install Vite plugins for core features

ophirbucai and others added 13 commits April 8, 2026 06:23
Revert regex match back to exact 'text="File uploaded"' now that
the source renders the original singular form.
…rement

Compile .po → .mjs at admin build time via lingui compile --namespace es.
Import pre-compiled JS catalogs instead of raw .po files — no Lingui
Vite plugin needed in consuming sites.

- Add lingui compile to admin build script
- Switch admin.astro and useLocale.ts imports from .po to .mjs
- Remove @lingui/vite-plugin from demos/simple and e2e fixture configs
- Compiled messages.mjs committed as build artifact alongside .po source
Import .po in dev (Vite plugin compiles on the fly for instant
translation feedback) and pre-compiled .mjs in production (no plugin
needed). Conditional via import.meta.env.DEV.
Dev mode imports .po directly — the Vite plugin compiles on the fly.
Restore the plugin and its dependency in demos/simple.
Dev mode: edit .po, refresh browser — no compile or restart needed.
Production: lingui compile before committing generates .mjs.
Simplify steps and update common mistakes.
E2E runs astro dev where import.meta.env.DEV is true, importing .po
directly. Add the Vite plugin to compile .po on the fly.

Also remove messages.mjs from git tracking and gitignore it — it's a
build artifact generated by lingui compile, not a source file.
Remove the import.meta.env.DEV conditional and Lingui Vite plugin
from all configs. All environments import .mjs (pre-compiled by
lingui compile during admin build). No Vite plugin needed anywhere.
Add @lingui/babel-plugin-lingui-macro to react() config in all demos
so Lingui t`` macros compile correctly during development.
Add a Vite plugin to the EmDash integration that compiles Lingui
macros (t``, <Trans>, <Plural>) when the admin is aliased to source
in dev mode. Uses enforce: "pre" to run before the React JSX
transform. No Babel config needed in consuming sites.
…ecture

Reflects the new automatic macro compilation pipeline: tsdown plugin
for build, Vite plugin for dev. No consumer Babel config needed.
@ophirbucai ophirbucai force-pushed the feat/admin-i18n-lingui branch from cba588b to 72032a9 Compare April 8, 2026 03:28
@ophirbucai
Copy link
Copy Markdown
Author

Hi,
I realized that the lingui babel plugin needed to be injected via the integration so it doesn't have to be added to every client facing app, demo & E2E.
So I added it to both the admin's tsdown build (for the published package) and the integration's Vite config - runs before JSX transformation takes place.

Updated the SKILL.md to reflect the changes

Key commits:

ascorbic and others added 12 commits April 8, 2026 07:50
Prevents @babel/types from being bundled into dist — it's a dev-time
dependency used only by the emdash-lingui-macro Vite plugin.
… e2e

These devDependencies were added when the Babel plugin was configured
in each consumer's astro.config. Now that the integration handles
macro compilation automatically, consumers don't need it.
Variable dynamic imports fail for newly added locales because Vite
can't resolve the package-qualified path at analysis time. Glob
imports let Vite watch for new catalog files and serve them in dev
without a server restart.
Rename lingui-macro-patterns/skill.md to SKILL.md to match project
convention. Note in adding-admin-locale that lingui compile is needed
after editing .po files to see changes in dev.
- Add locale:compile and locale:copy scripts
- Build now runs compile → tsdown → copy → tailwind
- Change ./locales/* export from src/ to dist/ for npm publish
- Remove locale:check (CI concern, not ours)
- Update SKILL.md to match new script names and correct stale claims
ContentList: View published aria-label
MenuList: Primary Navigation placeholder
MenuEditor: Home placeholder
@ophirbucai
Copy link
Copy Markdown
Author

@ascorbic Ready to review

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Platform UI internationalization — admin interface is English-only

3 participants